/* 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, beforeEach, describe, expect, it, vi } from 'vitest' import type * as GitUsernameModule from '../git/git-username' import { performance } from 'node:perf_hooks' import { EventEmitter } from 'node:events' import { randomUUID } from 'node:crypto' import { execFileSync } from 'node:child_process' import { mkdirSync } from 'node:fs' import { lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { homedir, tmpdir } from 'node:os' import { basename, join, win32 } from 'node:path' import { ipcMain } from 'electron' import type { FolderWorkspace, ProjectGroup, Tab, TerminalLayoutSnapshot, WorktreeLineage, WorktreeMeta, WorkspaceLineage, WorkspaceSessionState } from '../../shared/types' import { AGENT_STATUS_STALE_AFTER_MS } from '../../shared/agent-status-types' import { detectAgentStatusFromTitle, MAX_OSC_TITLE_CHARS } from '../../shared/agent-detection' import { addWorktree, assertWorktreeCleanForRemoval, listWorktrees, listWorktreesStrict, removeWorktree } from '../git/worktree' import * as gitRunner from '../git/runner' import { WORKTREE_PROCESS_SWEEP_TIMEOUT_MS, WORKTREE_TEARDOWN_RPC_MARGIN_MS } from './worktree-teardown' import { clearSubmodulePathsCacheForTests, listSubmodulePaths } from '../git/status' import { createSetupRunnerScript, getEffectiveHooks, getEffectiveHooksFromConfig, getDefaultTabsLaunch, hasHooksFile, loadHooks, parseOrcaYaml, runHook, shouldRunSetupForCreate } from '../hooks' import { getBaseRefDefault, getBranchConflictKind } from '../git/repo' import type { OrchestrationDb } from './orchestration/db' import type { MessagePriority, MessageRow, MessageType } from './orchestration/types' import { appendNormalizedToTailBuffer, appendRecentPtyPathCandidates, buildPreview, OrcaRuntimeService, recentTerminalPathCandidatesIncludePath, recentTerminalOutputIncludesPath, resolveWorktreeScanCacheTtlMs, type RuntimeTerminalAgentStatusEvent } from './orca-runtime' import { RecentPtyOutputBuffer } from './recent-pty-output-buffer' import { HeadlessEmulator } from '../daemon/headless-emulator' import { HEADLESS_RUNTIME_WINDOW_ID, type RuntimeMobileSessionTabsResult, type RuntimeTerminalCreate } from '../../shared/runtime-types' import type { TerminalSideEffectBatch } from '../../shared/terminal-side-effect-facts' import type { RuntimeClientEvent } from '../../shared/runtime-client-events' import { TERMINAL_INPUT_CHUNK_MAX_BYTES, TERMINAL_INPUT_MAX_BYTES, TERMINAL_INPUT_TOO_LARGE_ERROR } from '../../shared/terminal-input' import { MAX_QUICK_COMMANDS } from '../../shared/terminal-quick-commands' import { AGENT_PROMPT_BRACKETED_PASTE_END, AGENT_PROMPT_BRACKETED_PASTE_START, buildAgentPromptPasteBytes } from '../../shared/agent-prompt-injection' import { CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS } from '../../shared/clipboard-text' import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection' import { registerSshFilesystemProvider, unregisterSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch' import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch' import * as worktreePathComparison from '../ipc/worktree-path-comparison' import * as localWorktreeFilesystem from '../local-worktree-filesystem' import { DEFAULT_REPO_BADGE_COLOR, FLOATING_TERMINAL_WORKTREE_ID, getDefaultWorkspaceSession } from '../../shared/constants' import { advertisedUrlWatcher } from '../ports/advertised-url-watcher' import { makePaneKey } from '../../shared/stable-pane-id' import { SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV } from '../../shared/setup-agent-sequencing' import type { AgentSessionExecutionClaim, AgentSessionSurfaceBinding } from '../../shared/agent-session-host-authority' import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree-id' import { RpcDispatcher } from './rpc/dispatcher' import type { RpcRequest } from './rpc/core' import { TERMINAL_METHODS } from './rpc/methods/terminal' import { beginWatcherInstall } from '../ipc/watcher-removal-gate' import { _resetTerminalViewAttributesForTest, setTerminalViewAttributes } from './terminal-view-attribute-store' const ORIGINAL_PLATFORM = process.platform const ORIGINAL_PLATFORM_DESCRIPTOR = Object.getOwnPropertyDescriptor(process, 'platform') const removeWorktreeLinkedPathsMock = vi.hoisted(() => vi.fn()) const findExistingWorktreeSymlinkPathsMock = vi.hoisted(() => vi.fn()) const resolveLocalGitUsernameMock = vi.hoisted(() => vi.fn(async () => '')) vi.mock('../ipc/worktree-symlinks', () => ({ createWorktreeLinkedPaths: vi.fn(), findExistingWorktreeSymlinkPaths: findExistingWorktreeSymlinkPathsMock, removeWorktreeLinkedPaths: removeWorktreeLinkedPathsMock })) async function waitForMobileSessionTabsEvents( events: RuntimeMobileSessionTabsResult[], count: number ): Promise { await vi.waitFor(() => expect(events).toHaveLength(count)) } function setPlatform(platform: NodeJS.Platform): void { Object.defineProperty(process, 'platform', { configurable: true, value: platform }) } function resetPlatform(): void { if (ORIGINAL_PLATFORM_DESCRIPTOR) { Object.defineProperty(process, 'platform', ORIGINAL_PLATFORM_DESCRIPTOR) } } const electronMocks = vi.hoisted(() => { type Listener = (...args: unknown[]) => void const listeners = new Map>() const ipcMain = { on: vi.fn((channel: string, listener: Listener) => { const existing = listeners.get(channel) ?? new Set() existing.add(listener) listeners.set(channel, existing) return ipcMain }), removeListener: vi.fn((channel: string, listener: Listener) => { listeners.get(channel)?.delete(listener) return ipcMain }), emit: vi.fn((channel: string, ...args: unknown[]) => { for (const listener of listeners.get(channel) ?? []) { listener(...args) } return true }) } return { BrowserWindow: { fromId: vi.fn((_id: number): unknown => null) }, webContents: { fromId: vi.fn((_id: number): unknown => null) }, ipcMain, app: { getPath: vi.fn(() => '/tmp') } } }) const closeLocalWatcherForWorktreePathMock = vi.hoisted(() => vi.fn()) const closeRemoteWatcherForWorktreePathMock = vi.hoisted(() => vi.fn()) const restoreLocalWatcherAfterFailedRemovalMock = vi.hoisted(() => vi.fn()) const restoreRemoteWatcherAfterFailedRemovalMock = vi.hoisted(() => vi.fn()) const forgetLocalWatcherRemovalSnapshotMock = vi.hoisted(() => vi.fn()) const forgetRemoteWatcherRemovalSnapshotMock = vi.hoisted(() => vi.fn()) vi.mock('electron', () => electronMocks) vi.mock('../ipc/filesystem-watcher', () => ({ closeLocalWatcherForWorktreePath: closeLocalWatcherForWorktreePathMock, closeRemoteWatcherForWorktreePath: closeRemoteWatcherForWorktreePathMock, restoreLocalWatcherAfterFailedRemoval: restoreLocalWatcherAfterFailedRemovalMock, restoreRemoteWatcherAfterFailedRemoval: restoreRemoteWatcherAfterFailedRemovalMock, forgetLocalWatcherRemovalSnapshot: forgetLocalWatcherRemovalSnapshotMock, forgetRemoteWatcherRemovalSnapshot: forgetRemoteWatcherRemovalSnapshotMock })) const { MOCK_GIT_WORKTREES, addWorktreeMock, removeWorktreeMock, forceDeleteLocalBranchMock, computeWorktreePathMock, ensurePathWithinWorkspaceMock, sshGitProviders, sshProviderGenerations, getSshGitProviderMock, getSshGitProviderGenerationMock, registerSshGitProviderMock, unregisterSshGitProviderMock, getActiveMultiplexerMock, muxRequestMock, invalidateAuthorizedRootsCacheMock, prepareLocalWorktreeRootForRepoMock, createHostedReviewMock, getHostedReviewCreationEligibilityMock, getHostedReviewForBranchMock, getPRForBranchMock, getPRForBranchOutcomeMock, getRepoSlugMock, getRepoUpstreamMock, getGitHubWorkItemMock, getGitHubWorkItemByOwnerRepoMock, getGitHubWorkItemDetailsMock, getGitHubPRFileContentsMock, getGitHubPRChecksMock, rerunGitHubPRChecksMock, getGitHubPRCheckDetailsMock, getGitHubPRCommentsMock, resolveGitHubReviewThreadMock, setGitHubPRFileViewedMock, updateGitHubPRTitleMock, updateGitHubPRDetailsMock, mergeGitHubPRMock, setGitHubPRAutoMergeMock, updateGitHubPRStateMock, requestGitHubPRReviewersMock, removeGitHubPRReviewersMock, addGitHubPRReviewCommentMock, addGitHubPRReviewCommentReplyMock, listGitHubIssuesMock, listGitHubWorkItemsMock, countGitHubWorkItemsMock, createGitHubIssueMock, updateGitHubIssueMock, addGitHubIssueCommentMock, listGitHubLabelsMock, listGitHubAssignableUsersMock, detectInstalledAgentsWithShellPathHydrationMock, detectRemoteAgentsMock, markCodexProjectTrustedMock, markCopilotFolderTrustedMock, markCursorWorkspaceTrustedMock, listGitLabMergeRequestsMock, listGitLabWorkItemsMock, listGitLabIssuesMock, listGitLabLabelsMock, listGitLabTodosMock, getGitLabProjectRefForRemoteMock, getGitLabWorkItemByProjectRefMock, createGitLabIssueMock, updateGitLabIssueMock, addGitLabIssueCommentMock, addGitLabMRCommentMock, addGitLabMRInlineCommentMock, resolveGitLabMRDiscussionMock, getGitLabJobTraceMock, retryGitLabJobMock, mergeGitLabMRMock, closeGitLabMRMock, reopenGitLabMRMock, updateGitLabMRMock, getGlabKnownHostsMock, getGitLabWorkItemDetailsMock, updateGitLabMRReviewersMock, getIssueMock, deleteWorktreeHistoryDirMock } = vi.hoisted(() => { // Why: SSH runtime tests register providers via the public dispatcher API, so the mock needs the same registry semantics as the real module. const sshGitProviders = new Map() const sshProviderGenerations = new Map() return { MOCK_GIT_WORKTREES: [ { path: '/tmp/worktree-a', head: 'abc', branch: 'feature/foo', isBare: false, isMainWorktree: false } ], addWorktreeMock: vi.fn(), removeWorktreeMock: vi.fn(), forceDeleteLocalBranchMock: vi.fn(), computeWorktreePathMock: vi.fn(), ensurePathWithinWorkspaceMock: vi.fn(), sshGitProviders, sshProviderGenerations, getSshGitProviderMock: vi.fn((connectionId: string) => sshGitProviders.get(connectionId)), getSshGitProviderGenerationMock: vi.fn( (connectionId: string) => sshProviderGenerations.get(connectionId) ?? 0 ), registerSshGitProviderMock: vi.fn((connectionId: string, provider: unknown) => { sshGitProviders.set(connectionId, provider) sshProviderGenerations.set(connectionId, (sshProviderGenerations.get(connectionId) ?? 0) + 1) }), unregisterSshGitProviderMock: vi.fn((connectionId: string) => { if (sshGitProviders.delete(connectionId)) { sshProviderGenerations.set( connectionId, (sshProviderGenerations.get(connectionId) ?? 0) + 1 ) } }), getActiveMultiplexerMock: vi.fn(), muxRequestMock: vi.fn(), invalidateAuthorizedRootsCacheMock: vi.fn(), prepareLocalWorktreeRootForRepoMock: vi.fn(), createHostedReviewMock: vi.fn(), getHostedReviewCreationEligibilityMock: vi.fn(), getHostedReviewForBranchMock: vi.fn(), getPRForBranchMock: vi.fn().mockResolvedValue(null), getPRForBranchOutcomeMock: vi.fn().mockResolvedValue({ kind: 'no-pr', fetchedAt: 0 }), getRepoSlugMock: vi.fn().mockResolvedValue(null), getRepoUpstreamMock: vi.fn().mockResolvedValue(null), getGitHubWorkItemMock: vi.fn(), getGitHubWorkItemByOwnerRepoMock: vi.fn(), getGitHubWorkItemDetailsMock: vi.fn(), getGitHubPRFileContentsMock: vi.fn(), getGitHubPRChecksMock: vi.fn(), rerunGitHubPRChecksMock: vi.fn(), getGitHubPRCheckDetailsMock: vi.fn(), getGitHubPRCommentsMock: vi.fn(), resolveGitHubReviewThreadMock: vi.fn(), setGitHubPRFileViewedMock: vi.fn(), updateGitHubPRTitleMock: vi.fn(), updateGitHubPRDetailsMock: vi.fn(), mergeGitHubPRMock: vi.fn(), setGitHubPRAutoMergeMock: vi.fn(), updateGitHubPRStateMock: vi.fn(), requestGitHubPRReviewersMock: vi.fn(), removeGitHubPRReviewersMock: vi.fn(), addGitHubPRReviewCommentMock: vi.fn(), addGitHubPRReviewCommentReplyMock: vi.fn(), listGitHubIssuesMock: vi.fn(), listGitHubWorkItemsMock: vi.fn(), countGitHubWorkItemsMock: vi.fn(), createGitHubIssueMock: vi.fn(), updateGitHubIssueMock: vi.fn(), addGitHubIssueCommentMock: vi.fn(), listGitHubLabelsMock: vi.fn(), listGitHubAssignableUsersMock: vi.fn(), detectInstalledAgentsWithShellPathHydrationMock: vi.fn(), detectRemoteAgentsMock: vi.fn(), markCodexProjectTrustedMock: vi.fn(), markCopilotFolderTrustedMock: vi.fn(), markCursorWorkspaceTrustedMock: vi.fn(), listGitLabMergeRequestsMock: vi.fn(), listGitLabWorkItemsMock: vi.fn(), listGitLabIssuesMock: vi.fn(), listGitLabLabelsMock: vi.fn(), listGitLabTodosMock: vi.fn(), getGitLabProjectRefForRemoteMock: vi.fn(), getGitLabWorkItemByProjectRefMock: vi.fn(), createGitLabIssueMock: vi.fn(), updateGitLabIssueMock: vi.fn(), addGitLabIssueCommentMock: vi.fn(), addGitLabMRCommentMock: vi.fn(), addGitLabMRInlineCommentMock: vi.fn(), resolveGitLabMRDiscussionMock: vi.fn(), getGitLabJobTraceMock: vi.fn(), retryGitLabJobMock: vi.fn(), mergeGitLabMRMock: vi.fn(), closeGitLabMRMock: vi.fn(), reopenGitLabMRMock: vi.fn(), updateGitLabMRMock: vi.fn(), getGlabKnownHostsMock: vi.fn(), getGitLabWorkItemDetailsMock: vi.fn(), updateGitLabMRReviewersMock: vi.fn(), getIssueMock: vi.fn(), deleteWorktreeHistoryDirMock: vi.fn() } }) vi.mock('../git/worktree', () => ({ listWorktrees: vi.fn().mockResolvedValue(MOCK_GIT_WORKTREES), listWorktreesStrict: vi.fn().mockResolvedValue(MOCK_GIT_WORKTREES), assertWorktreeCleanForRemoval: vi.fn().mockResolvedValue(undefined), addWorktree: addWorktreeMock, removeWorktree: removeWorktreeMock, forceDeleteLocalBranch: forceDeleteLocalBranchMock })) vi.mock('../terminal-history', () => ({ deleteWorktreeHistoryDir: deleteWorktreeHistoryDirMock })) vi.mock('../providers/ssh-git-dispatch', () => ({ getSshGitProvider: getSshGitProviderMock, getSshGitProviderGeneration: getSshGitProviderGenerationMock, SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'Remote connection dropped. Click Reconnect on the SSH target before retrying.', requireSshGitProvider: (connectionId: string) => { const provider = getSshGitProviderMock(connectionId) if (!provider) { throw new Error( 'Remote connection dropped. Click Reconnect on the SSH target before retrying.' ) } return provider }, registerSshGitProvider: registerSshGitProviderMock, unregisterSshGitProvider: unregisterSshGitProviderMock })) vi.mock('../ipc/ssh', () => ({ getActiveMultiplexer: getActiveMultiplexerMock, getRegisteredSshState: () => ({ remotePlatform: 'linux' }) })) vi.mock('../ipc/preflight', () => ({ detectInstalledAgentsWithShellPathHydration: detectInstalledAgentsWithShellPathHydrationMock, detectRemoteAgents: detectRemoteAgentsMock })) vi.mock('../agent-trust-presets', () => ({ markCodexProjectTrusted: markCodexProjectTrustedMock, markCopilotFolderTrusted: markCopilotFolderTrustedMock, markCursorWorkspaceTrusted: markCursorWorkspaceTrustedMock })) vi.mock('../hooks', () => ({ buildPosixRunnerScript: (script: string) => `#!/usr/bin/env bash\nset -e\n${script}\n`, buildWindowsRunnerScript: (script: string) => `@echo off\r\n${script}\r\n`, createSetupRunnerScript: vi.fn(), getEffectiveHooks: vi.fn().mockReturnValue(null), getEffectiveHooksFromConfig: vi.fn().mockReturnValue(null), getDefaultTabCommandTrustContent: vi.fn( (hooks: { scripts?: { setup?: string } } | null) => hooks?.scripts?.setup?.trim() ?? '' ), getDefaultTabsLaunch: vi.fn().mockReturnValue(undefined), getSetupRunnerEnvVars: (_repo: never, worktreePath: string) => ({ ORCA_ROOT_PATH: '/remote/repo', ORCA_WORKTREE_PATH: worktreePath }), loadHooks: vi.fn().mockReturnValue(null), runHook: vi.fn().mockResolvedValue({ success: true, output: '' }), shouldRunSetupForCreate: vi .fn() .mockImplementation((_repo: never, decision: string) => decision === 'run'), getEffectiveSetupRunPolicy: vi.fn().mockReturnValue('auto'), hasHooksFile: vi.fn().mockReturnValue(false), parseOrcaYaml: vi.fn().mockReturnValue(null) })) 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, isENOENT: (error: unknown) => Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT'), resolveAuthorizedPath: vi.fn(async (pathValue: string) => pathValue) })) vi.mock('../worktree-root-preparation', () => ({ prepareLocalWorktreeRootForRepo: prepareLocalWorktreeRootForRepoMock })) vi.mock('../source-control/hosted-review-creation', () => ({ createHostedReview: createHostedReviewMock, getHostedReviewCreationEligibility: getHostedReviewCreationEligibilityMock })) vi.mock('../source-control/hosted-review', () => ({ getHostedReviewForBranch: getHostedReviewForBranchMock })) vi.mock('../github/client', async (importOriginal) => { const actual = (await importOriginal()) as Record return { ...actual, getPRForBranch: getPRForBranchMock, getPRForBranchOutcome: getPRForBranchOutcomeMock, getRepoSlug: getRepoSlugMock, getRepoUpstream: getRepoUpstreamMock, getWorkItem: getGitHubWorkItemMock, getWorkItemByOwnerRepo: getGitHubWorkItemByOwnerRepoMock, getPRChecks: getGitHubPRChecksMock, rerunPRChecks: rerunGitHubPRChecksMock, getPRCheckDetails: getGitHubPRCheckDetailsMock, getPRComments: getGitHubPRCommentsMock, resolveReviewThread: resolveGitHubReviewThreadMock, setPRFileViewed: setGitHubPRFileViewedMock, updatePRTitle: updateGitHubPRTitleMock, updatePRDetails: updateGitHubPRDetailsMock, mergePR: mergeGitHubPRMock, setPRAutoMerge: setGitHubPRAutoMergeMock, updatePRState: updateGitHubPRStateMock, requestPRReviewers: requestGitHubPRReviewersMock, removePRReviewers: removeGitHubPRReviewersMock, addPRReviewComment: addGitHubPRReviewCommentMock, addPRReviewCommentReply: addGitHubPRReviewCommentReplyMock, listIssues: listGitHubIssuesMock, listWorkItems: listGitHubWorkItemsMock, countWorkItems: countGitHubWorkItemsMock, getIssue: getIssueMock, createIssue: createGitHubIssueMock, updateIssue: updateGitHubIssueMock, addIssueComment: addGitHubIssueCommentMock, listLabels: listGitHubLabelsMock, listAssignableUsers: listGitHubAssignableUsersMock } }) vi.mock('../gitlab/client', async (importOriginal) => { const actual = (await importOriginal()) as Record return { ...actual, listMergeRequests: listGitLabMergeRequestsMock, listWorkItems: listGitLabWorkItemsMock, listIssues: listGitLabIssuesMock, listLabels: listGitLabLabelsMock, listTodos: listGitLabTodosMock, getProjectRefForRemote: getGitLabProjectRefForRemoteMock, getWorkItemByProjectRef: getGitLabWorkItemByProjectRefMock, createIssue: createGitLabIssueMock, updateIssue: updateGitLabIssueMock, addIssueComment: addGitLabIssueCommentMock, addMRComment: addGitLabMRCommentMock, addMRInlineComment: addGitLabMRInlineCommentMock, resolveMRDiscussion: resolveGitLabMRDiscussionMock, getJobTrace: getGitLabJobTraceMock, retryJob: retryGitLabJobMock, mergeMR: mergeGitLabMRMock, closeMR: closeGitLabMRMock, reopenMR: reopenGitLabMRMock, updateMR: updateGitLabMRMock, updateMRReviewers: updateGitLabMRReviewersMock } }) vi.mock('../gitlab/gl-utils', async (importOriginal) => { const actual = (await importOriginal()) as Record return { ...actual, getGlabKnownHosts: getGlabKnownHostsMock } }) vi.mock('../gitlab/work-item-details', async (importOriginal) => { const actual = (await importOriginal()) as Record return { ...actual, getWorkItemDetails: getGitLabWorkItemDetailsMock } }) vi.mock('../github/work-item-details', async (importOriginal) => { const actual = (await importOriginal()) as Record return { ...actual, getWorkItemDetails: getGitHubWorkItemDetailsMock, getPRFileContents: getGitHubPRFileContentsMock } }) vi.mock('../github/issues', async (importOriginal) => { const actual = (await importOriginal()) as Record return { ...actual, getIssue: getIssueMock } }) // Why: CLI worktree creation resolves a default against fabricated repo paths, so keep the async resolver deterministic. vi.mock('../git/repo', async (importOriginal) => { const actual = (await importOriginal()) as Record const actualGetBaseRefDefault = actual.getBaseRefDefault as ( path: string, options?: { wslDistro?: string } ) => Promise return { ...actual, // Why: fabricated local test repos need a deterministic default, while WSL coverage must still exercise the real async Git-options path. getBaseRefDefault: vi .fn() .mockImplementation((path: string, options?: { wslDistro?: string }) => options?.wslDistro ? actualGetBaseRefDefault(path, options) : Promise.resolve('origin/main') ), getBranchConflictKind: vi.fn().mockResolvedValue(null) } }) vi.mock('../git/git-username', async () => { const actual = await vi.importActual('../git/git-username') return { ...actual, resolveLocalGitUsername: resolveLocalGitUsernameMock } }) function resetRuntimeTestMocks(): void { resetPlatform() _resetTerminalViewAttributesForTest() advertisedUrlWatcher.clear() electronMocks.BrowserWindow.fromId.mockReset() electronMocks.BrowserWindow.fromId.mockReturnValue(null) electronMocks.webContents.fromId.mockReset() electronMocks.webContents.fromId.mockReturnValue(null) electronMocks.ipcMain.on.mockClear() electronMocks.ipcMain.removeListener.mockClear() electronMocks.ipcMain.emit.mockClear() closeLocalWatcherForWorktreePathMock.mockReset().mockResolvedValue(undefined) closeRemoteWatcherForWorktreePathMock.mockReset().mockResolvedValue(undefined) restoreLocalWatcherAfterFailedRemovalMock.mockReset().mockResolvedValue(undefined) restoreRemoteWatcherAfterFailedRemovalMock.mockReset().mockResolvedValue(undefined) forgetLocalWatcherRemovalSnapshotMock.mockReset() forgetRemoteWatcherRemovalSnapshotMock.mockReset() vi.mocked(listWorktrees).mockResolvedValue(MOCK_GIT_WORKTREES) vi.mocked(listWorktreesStrict).mockResolvedValue(MOCK_GIT_WORKTREES) vi.mocked(addWorktree).mockReset() vi.mocked(assertWorktreeCleanForRemoval).mockReset() vi.mocked(assertWorktreeCleanForRemoval).mockResolvedValue(undefined) vi.mocked(removeWorktree).mockReset() findExistingWorktreeSymlinkPathsMock.mockReset().mockResolvedValue([]) removeWorktreeLinkedPathsMock.mockReset() resolveLocalGitUsernameMock.mockReset().mockResolvedValue('') vi.mocked(forceDeleteLocalBranchMock).mockReset() vi.mocked(forceDeleteLocalBranchMock).mockResolvedValue(undefined) sshGitProviders.clear() sshProviderGenerations.clear() getSshGitProviderMock.mockReset() getSshGitProviderMock.mockImplementation((connectionId: string) => sshGitProviders.get(connectionId) ) registerSshGitProviderMock.mockReset() registerSshGitProviderMock.mockImplementation((connectionId: string, provider: unknown) => { sshGitProviders.set(connectionId, provider) sshProviderGenerations.set(connectionId, (sshProviderGenerations.get(connectionId) ?? 0) + 1) }) unregisterSshGitProviderMock.mockReset() unregisterSshGitProviderMock.mockImplementation((connectionId: string) => { if (sshGitProviders.delete(connectionId)) { sshProviderGenerations.set(connectionId, (sshProviderGenerations.get(connectionId) ?? 0) + 1) } }) muxRequestMock.mockReset() muxRequestMock.mockResolvedValue(undefined) getActiveMultiplexerMock.mockReset() getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) vi.mocked(createSetupRunnerScript).mockReset() vi.mocked(getEffectiveHooks).mockReset() vi.mocked(getEffectiveHooksFromConfig).mockReset() vi.mocked(getDefaultTabsLaunch).mockReset() vi.mocked(loadHooks).mockReset() vi.mocked(hasHooksFile).mockReset() vi.mocked(parseOrcaYaml).mockReset() vi.mocked(runHook).mockReset() vi.mocked(shouldRunSetupForCreate).mockReset() vi.mocked(shouldRunSetupForCreate).mockImplementation((_repo, decision) => decision === 'run') vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(getEffectiveHooksFromConfig).mockReturnValue(null) vi.mocked(getDefaultTabsLaunch).mockReturnValue(undefined) vi.mocked(loadHooks).mockReturnValue(null) vi.mocked(hasHooksFile).mockReturnValue(false) vi.mocked(parseOrcaYaml).mockReturnValue(null) computeWorktreePathMock.mockReset() ensurePathWithinWorkspaceMock.mockReset() invalidateAuthorizedRootsCacheMock.mockReset() prepareLocalWorktreeRootForRepoMock.mockReset().mockResolvedValue(undefined) createHostedReviewMock.mockReset() createHostedReviewMock.mockResolvedValue({ ok: true, provider: 'github', number: 1, url: 'https://example.com/pull/1' }) getHostedReviewCreationEligibilityMock.mockReset() getHostedReviewCreationEligibilityMock.mockResolvedValue({ provider: 'github', review: null, canCreate: true, blockedReason: null, nextAction: null, defaultBaseRef: 'main', head: 'feature/foo', title: null, body: null }) getHostedReviewForBranchMock.mockReset() getHostedReviewForBranchMock.mockResolvedValue(null) getPRForBranchMock.mockReset() getPRForBranchMock.mockResolvedValue(null) getPRForBranchOutcomeMock.mockReset() getPRForBranchOutcomeMock.mockResolvedValue({ kind: 'no-pr', fetchedAt: 0 }) getRepoSlugMock.mockReset() getRepoSlugMock.mockResolvedValue(null) getRepoUpstreamMock.mockReset() getRepoUpstreamMock.mockResolvedValue(null) getGitHubWorkItemMock.mockReset() getGitHubWorkItemMock.mockResolvedValue(null) getGitHubWorkItemByOwnerRepoMock.mockReset() getGitHubWorkItemByOwnerRepoMock.mockResolvedValue(null) getGitHubWorkItemDetailsMock.mockReset() getGitHubWorkItemDetailsMock.mockResolvedValue(null) getGitHubPRFileContentsMock.mockReset() getGitHubPRFileContentsMock.mockResolvedValue({ original: '', modified: '' }) getGitHubPRChecksMock.mockReset() getGitHubPRChecksMock.mockResolvedValue([]) rerunGitHubPRChecksMock.mockReset() rerunGitHubPRChecksMock.mockResolvedValue({ ok: true, count: 0 }) getGitHubPRCheckDetailsMock.mockReset() getGitHubPRCheckDetailsMock.mockResolvedValue(null) getGitHubPRCommentsMock.mockReset() getGitHubPRCommentsMock.mockResolvedValue([]) resolveGitHubReviewThreadMock.mockReset() resolveGitHubReviewThreadMock.mockResolvedValue(true) setGitHubPRFileViewedMock.mockReset() setGitHubPRFileViewedMock.mockResolvedValue(true) updateGitHubPRTitleMock.mockReset() updateGitHubPRTitleMock.mockResolvedValue(true) updateGitHubPRDetailsMock.mockReset() updateGitHubPRDetailsMock.mockResolvedValue({ ok: true }) mergeGitHubPRMock.mockReset() mergeGitHubPRMock.mockResolvedValue({ ok: true }) setGitHubPRAutoMergeMock.mockReset() setGitHubPRAutoMergeMock.mockResolvedValue({ ok: true }) updateGitHubPRStateMock.mockReset() updateGitHubPRStateMock.mockResolvedValue({ ok: true }) requestGitHubPRReviewersMock.mockReset() requestGitHubPRReviewersMock.mockResolvedValue({ ok: true }) removeGitHubPRReviewersMock.mockReset() removeGitHubPRReviewersMock.mockResolvedValue({ ok: true }) addGitHubPRReviewCommentMock.mockReset() addGitHubPRReviewCommentMock.mockResolvedValue({ ok: true }) addGitHubPRReviewCommentReplyMock.mockReset() addGitHubPRReviewCommentReplyMock.mockResolvedValue({ ok: true }) listGitHubIssuesMock.mockReset() listGitHubIssuesMock.mockResolvedValue({ items: [] }) listGitHubWorkItemsMock.mockReset() listGitHubWorkItemsMock.mockResolvedValue({ items: [] }) countGitHubWorkItemsMock.mockReset() countGitHubWorkItemsMock.mockResolvedValue(0) createGitHubIssueMock.mockReset() createGitHubIssueMock.mockResolvedValue({ ok: true, number: 1, url: 'https://example.com/1' }) updateGitHubIssueMock.mockReset() updateGitHubIssueMock.mockResolvedValue({ ok: true }) addGitHubIssueCommentMock.mockReset() addGitHubIssueCommentMock.mockResolvedValue({ ok: true }) listGitHubLabelsMock.mockReset() listGitHubLabelsMock.mockResolvedValue([]) listGitHubAssignableUsersMock.mockReset() listGitHubAssignableUsersMock.mockResolvedValue([]) detectInstalledAgentsWithShellPathHydrationMock.mockReset() detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValue([]) detectRemoteAgentsMock.mockReset() detectRemoteAgentsMock.mockResolvedValue([]) markCodexProjectTrustedMock.mockReset() markCopilotFolderTrustedMock.mockReset() markCursorWorkspaceTrustedMock.mockReset() listGitLabMergeRequestsMock.mockReset() listGitLabMergeRequestsMock.mockResolvedValue({ items: [] }) listGitLabWorkItemsMock.mockReset() listGitLabWorkItemsMock.mockResolvedValue({ items: [] }) listGitLabIssuesMock.mockReset() listGitLabIssuesMock.mockResolvedValue({ items: [] }) listGitLabLabelsMock.mockReset() listGitLabLabelsMock.mockResolvedValue(['bug']) listGitLabTodosMock.mockReset() listGitLabTodosMock.mockResolvedValue([]) getGitLabProjectRefForRemoteMock.mockReset() getGitLabProjectRefForRemoteMock.mockResolvedValue({ host: 'gitlab.example', path: 'group/repo' }) getGlabKnownHostsMock.mockReset() getGlabKnownHostsMock.mockResolvedValue(['gitlab.com']) getGitLabWorkItemByProjectRefMock.mockReset() getGitLabWorkItemByProjectRefMock.mockResolvedValue(null) createGitLabIssueMock.mockReset() createGitLabIssueMock.mockResolvedValue({ ok: true, number: 1, url: 'https://gitlab.example/i/1' }) updateGitLabIssueMock.mockReset() updateGitLabIssueMock.mockResolvedValue({ ok: true }) addGitLabIssueCommentMock.mockReset() addGitLabIssueCommentMock.mockResolvedValue({ ok: true }) addGitLabMRCommentMock.mockReset() addGitLabMRCommentMock.mockResolvedValue({ ok: true }) addGitLabMRInlineCommentMock.mockReset() addGitLabMRInlineCommentMock.mockResolvedValue({ ok: true }) resolveGitLabMRDiscussionMock.mockReset() resolveGitLabMRDiscussionMock.mockResolvedValue({ ok: true }) getGitLabJobTraceMock.mockReset() getGitLabJobTraceMock.mockResolvedValue({ ok: true, trace: 'log' }) retryGitLabJobMock.mockReset() retryGitLabJobMock.mockResolvedValue({ ok: true }) mergeGitLabMRMock.mockReset() mergeGitLabMRMock.mockResolvedValue({ ok: true }) closeGitLabMRMock.mockReset() closeGitLabMRMock.mockResolvedValue({ ok: true }) reopenGitLabMRMock.mockReset() reopenGitLabMRMock.mockResolvedValue({ ok: true }) updateGitLabMRMock.mockReset() updateGitLabMRMock.mockResolvedValue({ ok: true }) getGitLabWorkItemDetailsMock.mockReset() getGitLabWorkItemDetailsMock.mockResolvedValue({ body: 'Details' }) updateGitLabMRReviewersMock.mockReset() updateGitLabMRReviewersMock.mockResolvedValue({ ok: true, reviewers: [] }) getIssueMock.mockReset() getIssueMock.mockResolvedValue(null) } beforeEach(resetRuntimeTestMocks) afterEach(resetRuntimeTestMocks) function syncSinglePty( runtime: OrcaRuntimeService, ptyId: string | null = 'pty-1', options: { tabTitle?: string | null; paneTitle?: string | null } = {} ): void { runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: options.tabTitle ?? 'Codex', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId, paneTitle: options.paneTitle ?? null } ] }) } function makeDeferred(): { promise: Promise; resolve: () => void } { let resolve!: () => void const promise = new Promise((next) => { resolve = next }) return { promise, resolve } } function makeStatusFrame(index: number, first: boolean): string { const pad = '·'.repeat(60) const rows = [ `✻ 执行任务中… (esc to interrupt) [${index}] ${pad}`, ` ⎿ 正在分析代码库结构与依赖关系,请稍候… ${pad}`, ` ⎿ tokens: ${1000 + index * 137} · elapsed: ${index}s ${pad}` ] return `${first ? '' : '\x1b[2A'}\r${rows.map((row) => `\x1b[K${row}`).join('\r\n')}\r` } async function writeHeadless(emulator: HeadlessEmulator, data: string): Promise { await emulator.write(data) } function visibleNonEmptyLines(emulator: HeadlessEmulator): string[] { return emulator.getVisibleLines().filter((line) => line.length > 0) } async function parseHeadlessSnapshotLines( snapshot: { data: string; cols: number; rows: number }, display: { cols: number; rows: number } ): Promise { const restored = new HeadlessEmulator({ cols: display.cols, rows: display.rows }) try { restored.resize(snapshot.cols, snapshot.rows) await writeHeadless(restored, `\x1b[2J\x1b[3J\x1b[H${snapshot.data}`) restored.resize(display.cols, display.rows) return visibleNonEmptyLines(restored) } finally { restored.dispose() } } async function referenceStatusFrameLines( spawn: { cols: number; rows: number }, resized: { cols: number; rows: number } ): Promise { const truth = new HeadlessEmulator({ cols: spawn.cols, rows: spawn.rows }) try { await writeHeadless(truth, 'user@host % claude\r\n') truth.resize(resized.cols, resized.rows) for (let index = 0; index < 5; index += 1) { await writeHeadless(truth, makeStatusFrame(index, index === 0)) } return visibleNonEmptyLines(truth) } finally { truth.dispose() } } 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}` const TEST_FOLDER_PROJECT_GROUP_ID = 'folder-project-group-1' const TEST_FOLDER_WORKSPACE_ID = 'folder-workspace-1' const TEST_FOLDER_WORKSPACE_KEY = `folder:${TEST_FOLDER_WORKSPACE_ID}` const TEST_FOLDER_WORKSPACE_PATH = '/tmp/platform' const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ const HEADLESS_LEAF_ID = '11111111-1111-4111-8111-111111111111' const HEADLESS_SECOND_LEAF_ID = '22222222-2222-4222-8222-222222222222' function isOriginMainBaseRefProbe(args: string[]): boolean { return ( args[0] === 'rev-parse' && args[1] === '--verify' && (args.includes('origin/main') || args.includes('refs/remotes/origin/main') || args.includes('refs/remotes/origin/main^{commit}')) ) } function antigravityReadyScreen(model = 'Gemini 3.5 Flash (High)'): string { return [ 'Antigravity CLI 1.0.3', 'user@example.com (Antigravity Business)', model, '~/orca/workspaces/orca/agy-dispatch-issue', '>' ].join('\n') } function antigravityPromptBeforeModelReadyScreen(model = 'Gemini 3.5 Flash (High)'): string { return [ 'Antigravity CLI 1.0.3', 'user@example.com', '~/orca/workspaces/orca/agy-dispatch-issue', '', '', '', '', '>', '', '? for shortcuts', `\t\t ${model}`, '~/orca/workspaces/orca/agy-dispatch-issue', '', model, ' (Antigravity Business)' ].join('\n') } // Why: verbatim cursor-agent 2026.07 idle screen; the matcher keys on the "→" glyph, not the placeholder (which changes after the first turn). function cursorReadyScreen(): string { return [ 'Cursor Agent', 'v2026.07.09-a3815c0', 'Tip: Use /plan to plan execution and reach the right outcome faster.', '→ Plan, search, build anything', 'Composer 2.5 Fast Run Everything', '~/Documents/projects/AutoGenie · main' ].join('\n') } function cursorBusyScreen(): string { return [ 'Cursor Agent', 'v2026.07.09-a3815c0', '⠰⠳ Thinking 28.61k tokens', '→ Plan, search, build anything', 'Composer 2.5 Fast Run Everything', '~/Documents/projects/AutoGenie · main' ].join('\n') } // Why: these tests only need message-queue semantics; real SQLite would make them fail on unrelated native runtime ABI drift. class InMemoryOrchestrationMessages { private sequence = 0 private activeCoordinatorRun: { coordinator_handle: string } | null = null private messages: MessageRow[] = [] insertMessage(msg: { from: string to: string subject: string body?: string type?: MessageType priority?: MessagePriority threadId?: string payload?: string }): MessageRow { this.sequence += 1 const row: MessageRow = { id: `msg_${this.sequence}`, from_handle: msg.from, to_handle: msg.to, subject: msg.subject, body: msg.body ?? '', type: msg.type ?? 'status', priority: msg.priority ?? 'normal', thread_id: msg.threadId ?? null, payload: msg.payload ?? null, read: 0, sequence: this.sequence, created_at: '1970-01-01 00:00:00', delivered_at: null, sender_pane_key: null } this.messages.push(row) return row } getUnreadMessages(toHandle: string, types?: MessageType[]): MessageRow[] { return this.messages .filter( (message) => message.to_handle === toHandle && message.read === 0 && (!types || types.length === 0 || types.includes(message.type)) ) .sort((a, b) => a.sequence - b.sequence) } getUndeliveredUnreadMessages(toHandle: string, types?: MessageType[]): MessageRow[] { return this.getUnreadMessages(toHandle, types).filter((message) => !message.delivered_at) } setActiveCoordinatorRun(run: { coordinator_handle: string } | null): void { this.activeCoordinatorRun = run } getActiveCoordinatorRun(): { coordinator_handle: string } | null { return this.activeCoordinatorRun } markAsDelivered(ids: string[]): void { const deliveredIds = new Set(ids) for (const message of this.messages) { if (deliveredIds.has(message.id)) { message.delivered_at = '1970-01-01 00:00:00' } } } close(): void {} } function setInMemoryOrchestrationMessages( runtime: OrcaRuntimeService, db: InMemoryOrchestrationMessages ): void { runtime.setOrchestrationDb(db as unknown as OrchestrationDb) } function expectStablePaneKeyEnv(env: Record): string { expect(env.ORCA_TAB_ID).toMatch(UUID_RE) const leafId = env.ORCA_PANE_KEY?.slice(`${env.ORCA_TAB_ID}:`.length) expect(leafId).toMatch(UUID_RE) expect(env.ORCA_PANE_KEY).toBe(`${env.ORCA_TAB_ID}:${leafId}`) return env.ORCA_PANE_KEY } function createRuntime(): OrcaRuntimeService { return new OrcaRuntimeService(store) } async function withPlatform(platform: NodeJS.Platform, run: () => Promise): Promise { const original = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { configurable: true, value: platform }) try { return await run() } finally { if (original) { Object.defineProperty(process, 'platform', original) } } } function makeFolderProjectGroup(overrides: Partial = {}): ProjectGroup { return { id: TEST_FOLDER_PROJECT_GROUP_ID, name: 'Platform', parentPath: TEST_FOLDER_WORKSPACE_PATH, parentGroupId: null, createdFrom: 'folder-scan', tabOrder: 0, isCollapsed: false, color: null, createdAt: 1, updatedAt: 1, ...overrides } } function makeFolderWorkspace(overrides: Partial = {}): FolderWorkspace { return { ...overrides, id: overrides.id ?? TEST_FOLDER_WORKSPACE_ID, projectGroupId: overrides.projectGroupId ?? TEST_FOLDER_PROJECT_GROUP_ID, name: overrides.name ?? 'Refund fix', folderPath: overrides.folderPath ?? TEST_FOLDER_WORKSPACE_PATH, linkedTask: overrides.linkedTask ?? null, comment: overrides.comment ?? '', isArchived: overrides.isArchived ?? false, isUnread: overrides.isUnread ?? false, isPinned: overrides.isPinned ?? false, sortOrder: overrides.sortOrder ?? 0, lastActivityAt: overrides.lastActivityAt ?? 1, createdAt: overrides.createdAt ?? 1, updatedAt: overrides.updatedAt ?? 1 } } function createFolderWorkspaceRuntimeStore( folderWorkspace: FolderWorkspace = makeFolderWorkspace(), projectGroup: ProjectGroup = makeFolderProjectGroup() ) { return { ...store, getProjectGroups: () => [projectGroup], getFolderWorkspaces: () => [folderWorkspace] } } function makeRpcRequest(method: string, params?: unknown): RpcRequest { return { id: 'req-1', authToken: 'tok', method, params } } function makeWorktreeMeta(overrides: Partial = {}): WorktreeMeta { return { displayName: '', comment: '', linkedIssue: null, linkedPR: null, linkedLinearIssue: null, linkedGitLabMR: null, linkedGitLabIssue: null, isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 0, ...overrides } } function makeWorktreeInfo( path: string, head = 'head' ): { path: string head: string branch: string isBare: boolean isMainWorktree: boolean } { return { path, head, branch: 'main', isBare: false, isMainWorktree: path.endsWith('/repo') } } function deferred(): { promise: Promise resolve: (value: T) => void reject: (error: unknown) => void } { let resolve!: (value: T) => void let reject!: (error: unknown) => void const promise = new Promise((res, rej) => { resolve = res reject = rej }) return { promise, resolve, reject } } function createStaleRuntimeWorktreeStore( worktreeId: string, metaOverrides: Partial = {} ) { const metaById: Record = { [worktreeId]: makeWorktreeMeta(metaOverrides) } const removeWorktreeMeta = vi.fn((id: string) => { delete metaById[id] }) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (id: string) => metaById[id], setWorktreeMeta: (id: string, meta: Partial) => { metaById[id] = { ...(metaById[id] ?? makeWorktreeMeta()), ...meta } return metaById[id] }, removeWorktreeMeta } return { runtimeStore, removeWorktreeMeta } } 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, linkedGitLabMR: null, linkedGitLabIssue: 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: () => {}, getSparsePresets: () => [], saveSparsePreset: (preset: unknown) => preset as never, getGitHubCache: () => undefined as never, getSettings: () => ({ workspaceDir: '/tmp/workspaces', nestWorkspaces: false, refreshLocalBaseRefOnWorktreeCreate: false, branchPrefix: 'none', branchPrefixCustom: '' }), getProjects: () => [] } function createRuntimeWithSshLease( ptyId: string, tabId: string, state: 'expired' | 'terminated' = 'expired' ): OrcaRuntimeService { const now = Date.now() return new OrcaRuntimeService({ ...store, getSshRemotePtyLeases: () => [ { targetId: 'ssh-target', ptyId, worktreeId: TEST_WORKTREE_ID, tabId, leafId: HEADLESS_LEAF_ID, state, createdAt: now, updatedAt: now } ] }) } async function createExplicitAgentStatusHarness(options: { getForegroundProcess: (ptyId: string) => Promise confirmForegroundProcess?: (ptyId: string) => Promise title?: string }): Promise<{ runtime: OrcaRuntimeService handle: string syncPty: (ptyId: string | null) => void }> { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: options.getForegroundProcess, confirmForegroundProcess: options.confirmForegroundProcess }) runtime.attachWindow(1) const syncPty = (ptyId: string | null): void => { runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: options.title ?? 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId } ] }) } syncPty('pty-1') const [terminal] = (await runtime.listTerminals()).terminals return { runtime, handle: terminal.handle, syncPty } } function makeHeadlessTerminalLayout( ptyIdsByLeafId: Record ): TerminalLayoutSnapshot { const leafIds = Object.keys(ptyIdsByLeafId) const firstLeafId = leafIds[0] ?? HEADLESS_LEAF_ID return { root: leafIds.length > 1 ? { type: 'split', direction: 'vertical', first: { type: 'leaf', leafId: leafIds[0]! }, second: { type: 'leaf', leafId: leafIds[1]! } } : { type: 'leaf', leafId: firstLeafId }, activeLeafId: firstLeafId, expandedLeafId: null, ptyIdsByLeafId: Object.fromEntries( Object.entries(ptyIdsByLeafId).filter( (entry): entry is [string, string] => typeof entry[1] === 'string' ) ) } } function makeRuntimeStoreWithWorkspaceSession(initialSession: WorkspaceSessionState): { runtimeStore: typeof store & { getWorkspaceSession: () => WorkspaceSessionState setWorkspaceSession: ReturnType persistPtyBinding: ReturnType } getSession: () => WorkspaceSessionState } { let session = initialSession const runtimeStore = { ...store, getWorkspaceSession: () => session, setWorkspaceSession: vi.fn((next: WorkspaceSessionState) => { session = next }), persistPtyBinding: vi.fn( (args: { worktreeId: string; tabId: string; leafId: string; ptyId: string }) => { const tabs = session.tabsByWorktree[args.worktreeId] ?? [] session = { ...session, tabsByWorktree: { ...session.tabsByWorktree, [args.worktreeId]: tabs.map((tab) => tab.id === args.tabId ? { ...tab, ptyId: args.ptyId } : tab ) }, terminalLayoutsByTabId: { ...session.terminalLayoutsByTabId, [args.tabId]: { ...(session.terminalLayoutsByTabId[args.tabId] ?? { root: { type: 'leaf', leafId: args.leafId }, activeLeafId: args.leafId, expandedLeafId: null }), ptyIdsByLeafId: { ...session.terminalLayoutsByTabId[args.tabId]?.ptyIdsByLeafId, [args.leafId]: args.ptyId } } } } } ) } return { runtimeStore, getSession: () => session } } function makeWorkspaceSessionWithHeadlessTerminal( overrides: Partial = {} ): WorkspaceSessionState { const layout = makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'persisted-pty' }) return { ...getDefaultWorkspaceSession(), activeRepoId: TEST_REPO_ID, activeWorktreeId: TEST_WORKTREE_ID, activeTabId: 'host-tab', activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'host-tab' }, tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'persisted-pty', worktreeId: TEST_WORKTREE_ID, title: 'Persisted Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': layout }, ...overrides } } 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.dedupeWorktreeCreate', () => { it('coalesces concurrent creates that share a clientMutationId', async () => { const runtime = new OrcaRuntimeService(store) let calls = 0 const factory = (): Promise<{ worktree: { id: string } }> => { calls += 1 return Promise.resolve({ worktree: { id: 'wt' } }) } const [a, b] = await Promise.all([ runtime.dedupeWorktreeCreate('id:r', 'key-1', factory), runtime.dedupeWorktreeCreate('id:r', 'key-1', factory) ]) expect(calls).toBe(1) expect(a).toBe(b) }) it('reuses a settled success for a retry whose response was lost in a cutover', async () => { const runtime = new OrcaRuntimeService(store) let calls = 0 const factory = (): Promise<{ worktree: { id: string } }> => { calls += 1 return Promise.resolve({ worktree: { id: `wt-${calls}` } }) } const first = await runtime.dedupeWorktreeCreate('id:r', 'key-1', factory) const retried = await runtime.dedupeWorktreeCreate('id:r', 'key-1', factory) expect(calls).toBe(1) expect(retried).toEqual(first) }) it('expires settled successes after the reconnect window', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) let calls = 0 const factory = (): Promise<{ n: number }> => Promise.resolve({ n: (calls += 1) }) await runtime.dedupeWorktreeCreate('id:r', 'key-1', factory) await vi.advanceTimersByTimeAsync(59_999) await runtime.dedupeWorktreeCreate('id:r', 'key-1', factory) expect(calls).toBe(1) await vi.advanceTimersByTimeAsync(1) await runtime.dedupeWorktreeCreate('id:r', 'key-1', factory) expect(calls).toBe(2) } finally { vi.useRealTimers() } }) it('drops a failed create so a genuine retry starts fresh', async () => { const runtime = new OrcaRuntimeService(store) let calls = 0 const factory = (): Promise => { calls += 1 return Promise.reject(new Error(`boom-${calls}`)) } await expect(runtime.dedupeWorktreeCreate('id:r', 'key-1', factory)).rejects.toThrow('boom-1') await expect(runtime.dedupeWorktreeCreate('id:r', 'key-1', factory)).rejects.toThrow('boom-2') expect(calls).toBe(2) }) it('never dedupes across repos or when no clientMutationId is supplied', async () => { const runtime = new OrcaRuntimeService(store) let calls = 0 const factory = (): Promise<{ n: number }> => { calls += 1 return Promise.resolve({ n: calls }) } await runtime.dedupeWorktreeCreate('id:a', 'key-1', factory) await runtime.dedupeWorktreeCreate('id:b', 'key-1', factory) await runtime.dedupeWorktreeCreate('id:a', undefined, factory) await runtime.dedupeWorktreeCreate('id:a', undefined, factory) expect(calls).toBe(4) }) }) describe('OrcaRuntimeService', () => { it('projects runtime-backed settings to paired clients', () => { const terminalQuickCommands = [ { id: 'review', label: 'Review', action: 'agent-prompt' as const, agent: 'codex' as const, prompt: 'Review this diff', scope: { type: 'global' as const } } ] const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), experimentalNewWorktreeCardStyle: true, compactWorktreeCards: true, minimaxGroupId: 'group-42', minimaxUsageModels: 'general,abab6.5', terminalQuickCommands }) } as never) expect(runtime.getClientSettings()).toMatchObject({ experimentalNewWorktreeCardStyle: true, compactWorktreeCards: true, minimaxGroupId: 'group-42', minimaxUsageModels: 'general,abab6.5' }) expect(runtime.getClientSettings()).not.toHaveProperty('terminalQuickCommands') expect(runtime.getClientTerminalQuickCommands()).toEqual(terminalQuickCommands) }) it('updates quick commands without widening general paired settings payloads', () => { const existing = { id: 'review', label: 'Review', action: 'terminal-command' as const, command: 'pnpm review', appendEnter: true, scope: { type: 'global' as const } } let settings = { ...store.getSettings(), terminalQuickCommands: [existing] } const updateSettings = vi.fn((updates: Partial) => { settings = { ...settings, ...updates } }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => settings, updateSettings } as never) const command = { id: 'status', label: 'Status', action: 'terminal-command' as const, command: 'git status', appendEnter: true, scope: { type: 'global' as const } } const commands = [existing, command] expect(runtime.updateClientTerminalQuickCommands({ type: 'upsert', command })).toEqual(commands) expect(updateSettings).toHaveBeenCalledWith( { terminalQuickCommands: commands }, { notifyListeners: true } ) expect(runtime.getClientSettings()).not.toHaveProperty('terminalQuickCommands') }) it('rejects a concurrent add after the quick command limit is reached', () => { const terminalQuickCommands = Array.from({ length: MAX_QUICK_COMMANDS }, (_, index) => ({ id: `command-${index}`, label: `Command ${index}`, action: 'terminal-command' as const, command: 'true', appendEnter: true, scope: { type: 'global' as const } })) const updateSettings = vi.fn() const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), terminalQuickCommands }), updateSettings } as never) expect(() => runtime.updateClientTerminalQuickCommands({ type: 'upsert', command: { id: 'one-too-many', label: 'One too many', action: 'terminal-command', command: 'true', appendEnter: true, scope: { type: 'global' } } }) ).toThrow('Quick command limit reached') expect(updateSettings).not.toHaveBeenCalled() }) it('accepts runtime-backed setting updates from paired clients', () => { let settings = { ...store.getSettings(), experimentalNewWorktreeCardStyle: false, compactWorktreeCards: false, minimaxGroupId: '', minimaxUsageModels: 'general' } const updateSettings = vi.fn((updates: Partial) => { settings = { ...settings, ...updates } return settings }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => settings, updateSettings } as never) expect( runtime.updateClientSettings({ experimentalNewWorktreeCardStyle: true, compactWorktreeCards: true, minimaxGroupId: 'group-42', minimaxUsageModels: 'general,abab6.5' }) ).toMatchObject({ experimentalNewWorktreeCardStyle: true, compactWorktreeCards: true, minimaxGroupId: 'group-42', minimaxUsageModels: 'general,abab6.5' }) expect(updateSettings).toHaveBeenCalledWith( { experimentalNewWorktreeCardStyle: true, compactWorktreeCards: true, minimaxGroupId: 'group-42', minimaxUsageModels: 'general,abab6.5' }, { notifyListeners: true } ) expect(runtime.getClientSettings()).toMatchObject({ experimentalNewWorktreeCardStyle: true, compactWorktreeCards: true, minimaxGroupId: 'group-42', minimaxUsageModels: 'general,abab6.5' }) }) it('rejects relative paths for runtime nested repo scan/import', async () => { const runtime = new OrcaRuntimeService({ ...store, createProjectGroup: vi.fn(), moveProjectToGroup: vi.fn() } as never) await expect(runtime.scanNestedRepos('relative/project')).rejects.toThrow( 'Project path must be an absolute path' ) await expect( runtime.importNestedRepos({ parentPath: 'relative/project', groupName: 'Project', projectPaths: ['relative/project/api'], mode: 'group' }) ).rejects.toThrow('Project path must be an absolute path') }) it('starts unavailable with no authoritative window', () => { const runtime = createRuntime() expect(runtime.getStatus()).toMatchObject({ graphStatus: 'unavailable', authoritativeWindowId: null, desktopWindowStatus: 'openable', rendererGraphEpoch: 0 }) expect(runtime.getRuntimeId()).toBeTruthy() }) it('reports runtime protocol, capabilities, and mobile aliases on status', () => { const runtime = createRuntime() const status = runtime.getStatus() expect(typeof status.runtimeProtocolVersion).toBe('number') expect(typeof status.minCompatibleRuntimeClientVersion).toBe('number') expect(status.runtimeProtocolVersion).toBe(status.protocolVersion) expect(status.minCompatibleRuntimeClientVersion).toBe(status.minCompatibleMobileVersion) expect(status.capabilities).toContain('terminal.binary-stream.v1') expect(status.capabilities).toContain('workspace-ports.v1') expect(status.capabilities).toContain('mobile.tasks.v1') expect(status.capabilities).toContain('terminal.quick-commands.v1') expect(status.capabilities).toContain('worktree.create-idempotency.v1') expect(status.capabilities).toContain('files.mutation-ownership.v1') expect(status.capabilities).toContain('project-host-setup.v1') expect(status.capabilities).toContain('linear.issue-attribute-filter.v1') expect(status.capabilities).not.toContain('browser.screencast.v1') expect(typeof status.protocolVersion).toBe('number') expect(typeof status.minCompatibleMobileVersion).toBe('number') expect(status.protocolVersion).toBeGreaterThanOrEqual(1) expect(status.minCompatibleMobileVersion).toBeGreaterThanOrEqual(0) }) it('reports the configured Windows terminal shell on status', () => { const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), terminalWindowsShell: 'wsl.exe' }) } as never) expect(runtime.getStatus().terminalWindowsShell).toBe('wsl.exe') }) it('reports floating workspace availability from settings on status', () => { expect(createRuntime().getStatus().floatingWorkspaceEnabled).toBe(true) const disabledRuntime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), floatingTerminalEnabled: false }) } as never) expect(disabledRuntime.getStatus().floatingWorkspaceEnabled).toBe(false) }) it('polls floating tabs with targeted PTY liveness and no repo/provider inventory', async () => { const getRepos = vi.fn(store.getRepos) const listProcesses = vi.fn().mockResolvedValue([]) const floatingPtyId = `${FLOATING_TERMINAL_WORKTREE_ID}@@pty-1` const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ activeRepoId: null, activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID, activeTabIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-tab' }, tabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [ { id: 'floating-tab', ptyId: floatingPtyId, worktreeId: FLOATING_TERMINAL_WORKTREE_ID, title: 'Floating Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'floating-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: floatingPtyId }) } }) ) const runtime = new OrcaRuntimeService({ ...runtimeStore, getRepos } as never) const ptyController = { livePtyIds: new Set([floatingPtyId]), write: () => true, kill: () => true, getForegroundProcess: async () => null, hasPty(this: { livePtyIds: Set }, ptyId: string) { return this.livePtyIds.has(ptyId) }, listProcesses } const hasPty = vi.spyOn(ptyController, 'hasPty') runtime.setPtyController(ptyController) const tabs = await runtime.listMobileSessionTabs(`id:${FLOATING_TERMINAL_WORKTREE_ID}`) const terminals = await runtime.listTerminals(`id:${FLOATING_TERMINAL_WORKTREE_ID}`) await runtime.listMobileSessionTabs(`id:${FLOATING_TERMINAL_WORKTREE_ID}`) await runtime.listTerminals(`id:${FLOATING_TERMINAL_WORKTREE_ID}`) expect(tabs.tabs).toEqual([ expect.objectContaining({ type: 'terminal', parentTabId: 'floating-tab', status: 'ready', terminal: expect.any(String) }) ]) expect(terminals.terminals).toEqual([ expect.objectContaining({ worktreeId: FLOATING_TERMINAL_WORKTREE_ID, connected: true }) ]) expect(hasPty).toHaveBeenCalledTimes(4) expect(hasPty).toHaveBeenCalledWith(floatingPtyId) expect(listProcesses).not.toHaveBeenCalled() expect(getRepos).not.toHaveBeenCalled() }) it('advertises browser screencast only when a renderer window is available', () => { const runtime = createRuntime() electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) runtime.attachWindow(TEST_WINDOW_ID) expect(runtime.getStatus().capabilities).toContain('browser.screencast.v1') }) it('advertises headless browser capability when an offscreen backend backs a windowless host', () => { const runtime = createRuntime() runtime.setOffscreenBrowserBackend({ createTab: vi.fn(), closeTab: vi.fn() }) const capabilities = runtime.getStatus().capabilities // Headless serve can still create/stream pages, so screencast is supported... expect(capabilities).toContain('browser.screencast.v1') // ...and the headless marker tells clients not to fall back to a local tab. expect(capabilities).toContain('browser.headless.v1') expect(capabilities).toContain('browser.certificate-trust.v1') }) it('surfaces live offscreen load failures in headless browser snapshots', () => { const runtime = createRuntime() runtime.setOffscreenBrowserBackend({ createTab: vi.fn(), closeTab: vi.fn() }) runtime.setAgentBrowserBridge({ tabList: vi.fn(() => ({ tabs: [ { browserPageId: 'page-certificate-error', index: 0, url: 'https://localhost:3443/', title: 'Local HTTPS', active: true, loadError: { code: -202, description: 'ERR_CERT_AUTHORITY_INVALID', validatedUrl: 'https://localhost:3443/' }, certificateFailure: { challengeId: 'challenge-1', browserPageId: 'page-certificate-error', errorCode: -202, error: 'ERR_CERT_AUTHORITY_INVALID', origin: 'https://localhost:3443', displayHost: 'localhost:3443', canProceed: true, observedAt: 123 } } ] })) } as never) const browserTabs = runtime['buildHeadlessMobileSessionBrowserTabs'](TEST_WORKTREE_ID) expect(browserTabs).toContainEqual( expect.objectContaining({ type: 'browser', browserPageId: 'page-certificate-error', loadError: { code: -202, description: 'ERR_CERT_AUTHORITY_INVALID', validatedUrl: 'https://localhost:3443/' }, certificateFailure: { challengeId: 'challenge-1', browserPageId: 'page-certificate-error', errorCode: -202, error: 'ERR_CERT_AUTHORITY_INVALID', origin: 'https://localhost:3443', displayHost: 'localhost:3443', canProceed: true, observedAt: 123 } }) ) }) it('detects headless browser-tab changes by field, treating absent and null loadError alike', () => { const runtime = createRuntime() const base = { type: 'browser' as const, id: 'page-1', title: 'Local', browserWorkspaceId: 'page-1', browserPageId: 'page-1', url: 'https://localhost:3443/', loading: false, canGoBack: false, canGoForward: false, isActive: true } const err = { code: -202, description: 'ERR_CERT_AUTHORITY_INVALID', validatedUrl: 'https://localhost:3443/' } const certificateFailure = { challengeId: 'challenge-1', browserPageId: 'page-1', errorCode: -202, error: 'ERR_CERT_AUTHORITY_INVALID', origin: 'https://localhost:3443', displayHost: 'localhost:3443', canProceed: true, observedAt: 123 } const unchanged = (a: unknown[], b: unknown[]): boolean => runtime['headlessBrowserTabsUnchanged'](a as never, b as never) // Absent vs explicit null loadError are equivalent (the JSON.stringify trap). expect(unchanged([{ ...base }], [{ ...base, loadError: null }])).toBe(true) expect(unchanged([{ ...base, loadError: err }], [{ ...base, loadError: { ...err } }])).toBe( true ) // A load-error-only change (identical ids/order) must not be missed. expect(unchanged([{ ...base }], [{ ...base, loadError: err }])).toBe(false) expect( unchanged([{ ...base, loadError: err }], [{ ...base, loadError: { ...err, code: -200 } }]) ).toBe(false) expect( unchanged( [{ ...base, certificateFailure }], [{ ...base, certificateFailure: { ...certificateFailure } }] ) ).toBe(true) expect(unchanged([{ ...base }], [{ ...base, certificateFailure }])).toBe(false) expect( unchanged( [{ ...base, certificateFailure }], [{ ...base, certificateFailure: { ...certificateFailure, challengeId: 'challenge-2' } }] ) ).toBe(false) // Scalar and length changes are detected. expect(unchanged([{ ...base }], [{ ...base, title: 'Changed' }])).toBe(false) expect(unchanged([{ ...base }], [{ ...base, isActive: false }])).toBe(false) expect(unchanged([{ ...base }], [{ ...base }, { ...base, id: 'page-2' }])).toBe(false) }) it('does not advertise headless browser capability when a renderer window exists', () => { const runtime = createRuntime() electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) runtime.attachWindow(TEST_WINDOW_ID) runtime.setOffscreenBrowserBackend({ createTab: vi.fn(), closeTab: vi.fn() }) expect(runtime.getStatus().capabilities).not.toContain('browser.headless.v1') // Desktop webviews still host certificate trust, so the proceed capability stays advertised for remote clients controlling those pages. expect(runtime.getStatus().capabilities).toContain('browser.certificate-trust.v1') }) it('does not advertise certificate trust when no browser backend is available', () => { const runtime = createRuntime() expect(runtime.getStatus().capabilities).not.toContain('browser.certificate-trust.v1') expect(runtime.getStatus().capabilities).not.toContain('browser.screencast.v1') }) it('closes a worktree’s offscreen browser pages when its metadata is removed (leak fix)', () => { const runtime = createRuntime() const closeTab = vi.fn().mockResolvedValue(undefined) runtime.setOffscreenBrowserBackend({ createTab: vi.fn(), closeTab }) runtime.setAgentBrowserBridge({ tabList: vi.fn((worktreeId: string) => worktreeId === TEST_WORKTREE_ID ? { tabs: [{ browserPageId: 'page-a' }, { browserPageId: 'page-b' }] } : { tabs: [] } ) } as never) runtime['removeWorktreeMetadataAndHistory'](store as never, TEST_WORKTREE_ID) expect(closeTab).toHaveBeenCalledWith('page-a') expect(closeTab).toHaveBeenCalledWith('page-b') expect(closeTab).toHaveBeenCalledTimes(2) }) 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('transfers authority from the headless sentinel to the first real window', () => { const runtime = createRuntime() electronMocks.BrowserWindow.fromId.mockImplementation((windowId: number) => windowId === TEST_WINDOW_ID ? ({ isDestroyed: () => false } as never) : null ) runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) runtime.attachWindow(TEST_WINDOW_ID) runtime.attachWindow(2) expect(runtime.getStatus()).toMatchObject({ authoritativeWindowId: TEST_WINDOW_ID, desktopWindowStatus: 'available', graphStatus: 'reloading', rendererGraphEpoch: 1 }) }) it('marks live headless PTYs for renderer reattach before desktop promotion', () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ activeWorktreeIdsOnShutdown: [] }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) runtime.registerPty('persisted-pty', TEST_WORKTREE_ID, null, { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }) runtime.attachWindow(TEST_WINDOW_ID) expect(getSession().activeWorktreeIdsOnShutdown).toEqual([TEST_WORKTREE_ID]) }) it('marks live bindings again when reopening after a promoted window closes', () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ activeWorktreeIdsOnShutdown: [] }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) runtime.registerPty('persisted-pty', TEST_WORKTREE_ID, null, { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }) runtime.attachWindow(TEST_WINDOW_ID) ;(runtimeStore.setWorkspaceSession as unknown as (next: WorkspaceSessionState) => void)({ ...getSession(), activeWorktreeIdsOnShutdown: [] }) runtime.markGraphUnavailable(TEST_WINDOW_ID) runtime.attachWindow(2) expect(getSession().activeWorktreeIdsOnShutdown).toEqual([TEST_WORKTREE_ID]) }) it('preserves live SSH session identities when promoting a headless runtime', () => { const remotePtyId = 'ssh:ssh-1@@persisted-pty' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ activeWorktreeIdsOnShutdown: [], activeConnectionIdsAtShutdown: [], remoteSessionIdsByTabId: {}, tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: remotePtyId, worktreeId: TEST_WORKTREE_ID, title: 'Remote Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: remotePtyId }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) runtime.registerPty(remotePtyId, TEST_WORKTREE_ID, 'ssh-1', { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }) runtime.attachWindow(TEST_WINDOW_ID) expect(getSession()).toMatchObject({ activeWorktreeIdsOnShutdown: [TEST_WORKTREE_ID], activeConnectionIdsAtShutdown: ['ssh-1'], remoteSessionIdsByTabId: { 'host-tab': remotePtyId } }) }) it('reports the activation gate state while no desktop window is available', () => { const runtime = new OrcaRuntimeService(store, undefined, { getDesktopWindowStatus: () => 'blocked' }) runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) expect(runtime.getStatus().desktopWindowStatus).toBe('blocked') }) 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) const events: RuntimeClientEvent[] = [] runtime.onClientEvent((event) => events.push(event)) 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' } ], mobileSessionTabs: [ { worktree: 'repo-1::/tmp/worktree-a', publicationEpoch: 'epoch-terminal-handle', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Claude', isActive: true } ] } ] }) 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', ptyId: 'pty-1', 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') const mobileTabs = await runtime.listMobileSessionTabs('branch:feature/foo') const mobileHandle = mobileTabs.tabs.find((tab) => tab.type === 'terminal')?.terminal if (!mobileHandle) { throw new Error('expected mobile terminal handle') } const processLists = [[{ id: 'pty-1', cwd: '/tmp/worktree-a', title: 'Claude' }], []] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) await runtime.sleepTerminalsForWorktree('branch:feature/foo') expect( events.find( (event) => event.type === 'worktreeTerminalSleepState' && event.phase === 'started' ) ).toMatchObject({ terminalHandles: [terminals.terminals[0].handle, mobileHandle].sort() }) }) it('surfaces stale terminal handles for stranded panes and recovers after same-pane wake', async () => { const runtime = new OrcaRuntimeService(store) const tabId = 'tab-1' const leafId = HEADLESS_LEAF_ID const paneKey = makePaneKey(tabId, leafId) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Codex', activeLeafId: leafId, layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-before-sleep' } ] }) const beforeSleep = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) const staleHandle = beforeSleep.terminals[0]?.handle ?? '' expect(staleHandle).toBeTruthy() // Why: `terminal.show` is read-only; only a later renderer wake/rebind graph publish can repair this pane's CLI handle surface. runtime.syncWindowGraph(1, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Codex', activeLeafId: leafId, layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: null } ] }) await expect(runtime.showTerminal(staleHandle)).rejects.toThrow('terminal_handle_stale') runtime.syncWindowGraph(1, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Codex', activeLeafId: leafId, layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-after-wake' } ] }) runtime.onPtyData('pty-after-wake', 'resumed in place\n', 123) const resolved = runtime.resolveTerminalPane(paneKey) expect(resolved).toMatchObject({ tabId, leafId, ptyId: 'pty-after-wake' }) await expect(runtime.showTerminal(resolved.handle)).resolves.toMatchObject({ handle: resolved.handle, tabId, leafId, ptyId: 'pty-after-wake', preview: 'resumed in place' }) }) it('rejects pane resolution when leaf and PTY ownership disagree', () => { const runtime = new OrcaRuntimeService(store) const tabId = 'tab-1' const leafId = HEADLESS_LEAF_ID const paneKey = makePaneKey(tabId, leafId) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Codex', activeLeafId: leafId, layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-mismatched-owner' } ] }) runtime.registerPty('pty-mismatched-owner', `${TEST_REPO_ID}::/tmp/other-worktree`) expect(() => runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID)).toThrow( 'terminal_not_found' ) }) it('recovers a disconnected pane through one HUB-owned replacement', async () => { const tabId = 'tab-recover' const runtime = createRuntimeWithSshLease('pty-expired', tabId) const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID) runtime.registerPty('pty-expired', TEST_WORKTREE_ID, null, { tabId, leafId: HEADLESS_LEAF_ID }) const expiredHandle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle runtime.onPtyExit('pty-expired', 0) const createTerminal = vi.spyOn(runtime, 'createTerminal').mockResolvedValue({ handle: 'term-replacement', tabId, paneKey, ptyId: 'pty-replacement', worktreeId: TEST_WORKTREE_ID, title: null, surface: 'background' }) await expect( runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, expiredHandle) ).resolves.toMatchObject({ handle: 'term-replacement', tabId, leafId: HEADLESS_LEAF_ID, worktreeId: TEST_WORKTREE_ID }) expect(createTerminal).toHaveBeenCalledWith(`id:${TEST_WORKTREE_ID}`, { tabId, leafId: HEADLESS_LEAF_ID, focus: false, persistHostSessionBinding: true }) }) it('rejects missing host panes without authoritative expired binding evidence', async () => { const runtime = new OrcaRuntimeService(store) const tabId = 'tab-missing' const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID) const createTerminal = vi.spyOn(runtime, 'createTerminal').mockResolvedValue({ handle: 'term-created', tabId, paneKey, ptyId: 'pty-created', worktreeId: TEST_WORKTREE_ID, title: null, surface: 'background' }) await expect(runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID)).rejects.toThrow( 'terminal_not_found' ) expect(createTerminal).not.toHaveBeenCalled() }) it('rejects recovery for live panes and mismatched worktrees', async () => { const runtime = new OrcaRuntimeService(store) const tabId = 'tab-live' const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID) runtime.registerPty('pty-live', TEST_WORKTREE_ID, null, { tabId, leafId: HEADLESS_LEAF_ID }) const liveHandle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle const createTerminal = vi.spyOn(runtime, 'createTerminal') await expect( runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, liveHandle) ).rejects.toThrow('terminal_not_recoverable') await expect( runtime.recoverTerminalPane(paneKey, `${TEST_REPO_ID}::/other`, liveHandle) ).rejects.toThrow('terminal_not_found') expect(createTerminal).not.toHaveBeenCalled() }) it('returns an already-connected replacement instead of spawning another pane', async () => { const runtime = new OrcaRuntimeService(store) const tabId = 'tab-cas' const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID) runtime.registerPty('pty-old', TEST_WORKTREE_ID, null, { tabId, leafId: HEADLESS_LEAF_ID }) const oldHandle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle runtime.onPtyExit('pty-old', 0) runtime.registerPty('pty-new', TEST_WORKTREE_ID, null, { tabId, leafId: HEADLESS_LEAF_ID }) const createTerminal = vi.spyOn(runtime, 'createTerminal') const recovered = await runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, oldHandle) expect(recovered.handle).not.toBe(oldHandle) expect(recovered.ptyId).toBe('pty-new') expect(createTerminal).not.toHaveBeenCalled() }) it('deduplicates concurrent pane recovery across stale viewer handles', async () => { const tabId = 'tab-concurrent' const runtime = createRuntimeWithSshLease('pty-expired', tabId) const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID) runtime.registerPty('pty-expired', TEST_WORKTREE_ID, null, { tabId, leafId: HEADLESS_LEAF_ID }) const expiredHandle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle runtime.onPtyExit('pty-expired', 0) let finishCreate!: (result: RuntimeTerminalCreate) => void const pendingCreate = new Promise((resolve) => { finishCreate = resolve }) const createTerminal = vi.spyOn(runtime, 'createTerminal').mockReturnValue(pendingCreate) const first = runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, expiredHandle) const second = runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, 'term-other-viewer') finishCreate({ handle: 'term-replacement', tabId, paneKey, ptyId: 'pty-replacement', worktreeId: TEST_WORKTREE_ID, title: null, surface: 'background' }) await expect(first).resolves.toEqual(expect.objectContaining({ handle: 'term-replacement' })) await expect(second).rejects.toThrow('terminal_not_found') expect(createTerminal).toHaveBeenCalledOnce() }) it('clears a failed pane recovery so a later reconnect can retry', async () => { const tabId = 'tab-retry' const runtime = createRuntimeWithSshLease('pty-expired', tabId) const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID) runtime.registerPty('pty-expired', TEST_WORKTREE_ID, null, { tabId, leafId: HEADLESS_LEAF_ID }) const expiredHandle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle runtime.onPtyExit('pty-expired', 0) const createTerminal = vi .spyOn(runtime, 'createTerminal') .mockRejectedValueOnce(new Error('relay_reconnecting')) .mockResolvedValueOnce({ handle: 'term-retry', tabId, paneKey, ptyId: 'pty-retry', worktreeId: TEST_WORKTREE_ID, title: null, surface: 'background' }) await expect( runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, expiredHandle) ).rejects.toThrow('relay_reconnecting') await expect( runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, expiredHandle) ).resolves.toMatchObject({ handle: 'term-retry' }) expect(createTerminal).toHaveBeenCalledTimes(2) }) it('does not recover a pane whose authoritative SSH lease was terminated', async () => { const tabId = 'tab-terminated' const runtime = createRuntimeWithSshLease('pty-terminated', tabId, 'terminated') const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID) runtime.registerPty('pty-terminated', TEST_WORKTREE_ID, null, { tabId, leafId: HEADLESS_LEAF_ID }) const handle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle runtime.onPtyExit('pty-terminated', 0) const createTerminal = vi.spyOn(runtime, 'createTerminal') await expect(runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, handle)).rejects.toThrow( 'terminal_not_recoverable' ) expect(createTerminal).not.toHaveBeenCalled() }) it('drops a stale leaf when a woken agent PTY is re-keyed to a new leaf on renderer reload', async () => { const runtime = createRuntime() const tabId = 'tab-1' // Why: the agent's pre-allocated ORCA_TERMINAL_HANDLE gives its PTY a handleByPtyId entry — the condition the reload preservation loop keys on. runtime.preAllocateHandleForPty('pty-agent') runtime.attachWindow(TEST_WINDOW_ID) runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: 'leaf-old', layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId: 'leaf-old', paneRuntimeId: 1, ptyId: 'pty-agent' } ] }) const before = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(before.terminals).toHaveLength(1) // Simulate agent sleep + mobile wake: the renderer cold-restores the pane under a NEW leafId while the SAME agent PTY stays live. runtime.markRendererReloading(TEST_WINDOW_ID) runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: 'leaf-new', layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId: 'leaf-new', paneRuntimeId: 2, ptyId: 'pty-agent' } ] }) const after = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) // Before the fix two leaves shared one PTY, so both adopted the same ptyId-keyed handle and paired clients crashed on a duplicate React key. expect(after.terminals).toHaveLength(1) expect(after.terminals[0].ptyId).toBe('pty-agent') // The shared handle must NOT have been invalidated — it belongs to leaf-new now. await expect(runtime.showTerminal(after.terminals[0].handle)).resolves.toMatchObject({ ptyId: 'pty-agent' }) }) it('still preserves a CLI agent leaf when the reloaded renderer has not rebound its PTY', async () => { const runtime = createRuntime() const tabId = 'tab-1' runtime.preAllocateHandleForPty('pty-agent') runtime.attachWindow(TEST_WINDOW_ID) runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: 'leaf-old', layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId: 'leaf-old', paneRuntimeId: 1, ptyId: 'pty-agent' } ] }) expect((await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)).terminals).toHaveLength(1) // Renderer reloads but hasn't rebound the pane (empty graph); the live CLI agent PTY + exported handle must survive. runtime.markRendererReloading(TEST_WINDOW_ID) runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [], leaves: [] }) const after = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(after.terminals).toHaveLength(1) expect(after.terminals[0].ptyId).toBe('pty-agent') }) it('invalidates a re-keyed leaf-unique handle so in-flight waiters fail fast', async () => { const runtime = createRuntime() const tabId = 'tab-1' // No preAllocateHandleForPty: a plain terminal's handle is leaf-unique, so a re-key leaves it with no next owner and it goes stale immediately. runtime.attachWindow(TEST_WINDOW_ID) runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Shell', activeLeafId: 'leaf-old', layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId: 'leaf-old', paneRuntimeId: 1, ptyId: 'pty-plain' } ] }) const before = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(before.terminals).toHaveLength(1) const staleHandle = before.terminals[0].handle const waiting = runtime.waitForTerminal(staleHandle, { condition: 'exit', timeoutMs: 30_000 }) // Re-key WITHOUT a renderer reload (e.g. a pane moved across tabs) while the same PTY stays live under a new leaf. runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Shell', activeLeafId: 'leaf-new', layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId: 'leaf-new', paneRuntimeId: 2, ptyId: 'pty-plain' } ] }) // The waiter must fail fast, not hang until timeout on a dead leaf. await expect(waiting).rejects.toThrow('terminal_handle_stale') const after = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(after.terminals).toHaveLength(1) expect(after.terminals[0].handle).not.toBe(staleHandle) }) it('keeps a live CLI waiter pending when a re-keyed shared handle transfers to the live leaf', async () => { const runtime = createRuntime() const tabId = 'tab-1' // Unlike the leaf-unique case, a shared ptyId-keyed handle re-keyed to a live leaf must transfer WITHOUT rejecting the in-flight CLI waiter. runtime.preAllocateHandleForPty('pty-agent') runtime.attachWindow(TEST_WINDOW_ID) runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: 'leaf-old', layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId: 'leaf-old', paneRuntimeId: 1, ptyId: 'pty-agent' } ] }) const before = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) const sharedHandle = before.terminals[0].handle const abort = new AbortController() let settled: 'resolved' | 'rejected' | null = null const waiting = runtime .waitForTerminal(sharedHandle, { condition: 'exit', timeoutMs: 30_000, signal: abort.signal }) .then( () => { settled = 'resolved' }, () => { settled = 'rejected' } ) // Re-key WITHOUT a renderer reload while the same agent PTY stays live under a new leaf. runtime.syncWindowGraph(TEST_WINDOW_ID, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: 'leaf-new', layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId: 'leaf-new', paneRuntimeId: 2, ptyId: 'pty-agent' } ] }) // Let any synchronous stale-handle rejection propagate. await new Promise((resolve) => setImmediate(resolve)) // The shared handle belongs to leaf-new now, so the CLI's live waiter must stay pending (a blanket invalidateLeafHandle would reject it as stale). expect(settled).toBeNull() // The same handle still resolves to the live PTY under the new leaf. await expect(runtime.showTerminal(sharedHandle)).resolves.toMatchObject({ ptyId: 'pty-agent' }) // Abort only for teardown; the assertion above already proved it was pending. abort.abort() await waiting expect(settled).toBe('rejected') }) it('emits one mobile session terminal tab per live PTY even if two tabs resolve to it', () => { const runtime = createRuntime() const internals = runtime as unknown as { recordPtyWorktree: ( ptyId: string, worktreeId: string, state?: Record ) => void mobileSessionTabsByWorktree: Map getMobileSessionTabsForWorktree: (worktreeId: string) => { tabs: { type: string; terminal?: string | null }[] } } // Two unclaimed live PTYs on the worktree; the worktree-only fallback binds either to a leafless tab that references it. internals.recordPtyWorktree('pty-shared', TEST_WORKTREE_ID, { connected: true }) internals.recordPtyWorktree('pty-other', TEST_WORKTREE_ID, { connected: true }) const terminalTab = (leafId: string, ptyId: string) => ({ type: 'terminal' as const, id: `term_dup::${leafId}`, parentTabId: 'term_dup', leafId, ptyId, title: 'Agent', isActive: leafId === 'leaf-new' }) // Two records (stale headless leaf + live leaf) both resolve to the SAME live PTY — the renderer-graph origin the leaf fix can't reach. internals.mobileSessionTabsByWorktree.set(TEST_WORKTREE_ID, { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer:test:1', snapshotVersion: 1, activeGroupId: null, activeTabId: 'term_dup::leaf-new', activeTabType: 'terminal', tabs: [terminalTab('leaf-new', 'pty-shared'), terminalTab('leaf-old', 'pty-shared')] }) const deduped = internals.getMobileSessionTabsForWorktree(TEST_WORKTREE_ID) const dedupedTerminals = deduped.tabs.filter((tab) => tab.type === 'terminal') expect(dedupedTerminals).toHaveLength(1) expect(dedupedTerminals[0].terminal).toBeTruthy() // Split siblings own DISTINCT PTYs, so they must never be collapsed. internals.mobileSessionTabsByWorktree.set(TEST_WORKTREE_ID, { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer:test:2', snapshotVersion: 2, activeGroupId: null, activeTabId: 'term_dup::leaf-new', activeTabType: 'terminal', tabs: [terminalTab('leaf-new', 'pty-shared'), terminalTab('leaf-old', 'pty-other')] }) const split = internals.getMobileSessionTabsForWorktree(TEST_WORKTREE_ID) expect(split.tabs.filter((tab) => tab.type === 'terminal')).toHaveLength(2) }) it('keeps targeted terminal lists from adopting controller PTYs for other worktrees', async () => { vi.mocked(listWorktrees).mockResolvedValue([ ...MOCK_GIT_WORKTREES, { path: '/tmp/worktree-b', head: 'def', branch: 'feature/bar', isBare: false, isMainWorktree: false }, { path: '/tmp/worktree-a/nested', head: 'ghi', branch: 'feature/nested', isBare: false, isMainWorktree: false } ]) const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'target-controller-pty', cwd: '/tmp/worktree-a/src', title: 'target' }, { id: 'other-controller-pty', cwd: '/tmp/worktree-b/src', title: 'other' }, { id: 'repo-1::/tmp/worktree-b@@other-controller-pty', cwd: '/tmp/worktree-a/src', title: 'prefixed other' }, { id: 'nested-controller-pty', cwd: '/tmp/worktree-a/nested/src', title: 'nested' } ] }) runtime.attachWindow(1) runtime.markGraphReady(1) const terminals = await runtime.listTerminals(`path:${TEST_WORKTREE_PATH}`) expect(terminals.terminals).toHaveLength(1) expect(terminals.terminals[0]).toMatchObject({ worktreeId: TEST_WORKTREE_ID, worktreePath: TEST_WORKTREE_PATH }) const internals = runtime as unknown as { ptysById: Map } expect(internals.ptysById.has('target-controller-pty')).toBe(true) expect(internals.ptysById.has('other-controller-pty')).toBe(false) expect(internals.ptysById.has('repo-1::/tmp/worktree-b@@other-controller-pty')).toBe(false) expect(internals.ptysById.has('nested-controller-pty')).toBe(false) }) it('keeps explicit-id terminal lists from resolving all worktrees', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue( new Error('all-worktree resolution should be skipped') ) const runtime = createRuntime() const ptyId = `${TEST_WORKTREE_ID}@@daemon-controller-pty` runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: ptyId, cwd: '/unresolved/cwd', title: 'daemon shell' }, { id: 'cwd-only-pty', cwd: TEST_WORKTREE_PATH, title: 'cwd shell' } ] }) const terminals = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(listWorktrees).not.toHaveBeenCalled() expect(terminals.terminals.map((terminal) => terminal.worktreeId)).toEqual([ TEST_WORKTREE_ID, TEST_WORKTREE_ID ]) const internals = runtime as unknown as { ptysById: Map } expect(internals.ptysById.has(ptyId)).toBe(true) expect(internals.ptysById.has('cwd-only-pty')).toBe(true) }) it('matches explicit-id cwd PTYs when the resolved worktree cache is incomplete', async () => { vi.mocked(listWorktrees).mockResolvedValueOnce([ { path: '/tmp/worktree-a/nested', head: 'ghi', branch: 'feature/nested', isBare: false, isMainWorktree: false } ]) const runtime = createRuntime() await runtime.listTerminals() vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue( new Error('explicit-id fallback should not rescan worktrees') ) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'cwd-only-pty', cwd: `${TEST_WORKTREE_PATH}/src`, title: 'cwd shell' }, { id: 'nested-controller-pty', cwd: `${TEST_WORKTREE_PATH}/nested/src`, title: 'nested' } ] }) const terminals = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(listWorktrees).not.toHaveBeenCalled() expect(terminals.terminals.map((terminal) => terminal.worktreeId)).toEqual([TEST_WORKTREE_ID]) const internals = runtime as unknown as { ptysById: Map } expect(internals.ptysById.has('cwd-only-pty')).toBe(true) expect(internals.ptysById.has('nested-controller-pty')).toBe(false) }) it('keeps explicit-id cold-cache terminal lists from adopting nested worktree PTYs', async () => { const nestedWorktreeId = `${TEST_REPO_ID}::${TEST_WORKTREE_PATH}/nested` vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue( new Error('explicit-id fallback should not rescan worktrees') ) const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => ({ [TEST_WORKTREE_ID]: store.getAllWorktreeMeta()[TEST_WORKTREE_ID], [nestedWorktreeId]: makeWorktreeMeta() }), getWorktreeMeta: (worktreeId: string) => ({ [TEST_WORKTREE_ID]: store.getAllWorktreeMeta()[TEST_WORKTREE_ID], [nestedWorktreeId]: makeWorktreeMeta() })[worktreeId] }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'cwd-only-pty', cwd: `${TEST_WORKTREE_PATH}/src`, title: 'cwd shell' }, { id: 'nested-controller-pty', cwd: `${TEST_WORKTREE_PATH}/nested/src`, title: 'nested' } ] }) const terminals = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(listWorktrees).not.toHaveBeenCalled() expect(terminals.terminals.map((terminal) => terminal.worktreeId)).toEqual([TEST_WORKTREE_ID]) const internals = runtime as unknown as { ptysById: Map } expect(internals.ptysById.has('cwd-only-pty')).toBe(true) expect(internals.ptysById.has('nested-controller-pty')).toBe(false) }) it('keeps explicit-id cold-cache terminal lists from classifying unrelated same-repo worktrees', async () => { const siblingWorktreePath = '/tmp/worktree-sibling' const siblingWorktreeId = `${TEST_REPO_ID}::${siblingWorktreePath}` vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue( new Error('explicit-id fallback should not rescan worktrees') ) const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => ({ [TEST_WORKTREE_ID]: store.getAllWorktreeMeta()[TEST_WORKTREE_ID], [siblingWorktreeId]: makeWorktreeMeta() }), getWorktreeMeta: (worktreeId: string) => ({ [TEST_WORKTREE_ID]: store.getAllWorktreeMeta()[TEST_WORKTREE_ID], [siblingWorktreeId]: makeWorktreeMeta() })[worktreeId] }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'target-cwd-pty', cwd: `${TEST_WORKTREE_PATH}/src`, title: 'target' }, { id: 'sibling-cwd-pty', cwd: `${siblingWorktreePath}/src`, title: 'sibling' } ] }) const terminals = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(listWorktrees).not.toHaveBeenCalled() expect(terminals.terminals.map((terminal) => terminal.worktreeId)).toEqual([TEST_WORKTREE_ID]) const internals = runtime as unknown as { ptysById: Map } expect(internals.ptysById.has('target-cwd-pty')).toBe(true) expect(internals.ptysById.has('sibling-cwd-pty')).toBe(false) }) it('ignores cwd-only controller PTYs for malformed explicit worktree IDs', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue( new Error('malformed explicit-id fallback should not rescan worktrees') ) const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'cwd-only-pty', cwd: `${TEST_WORKTREE_PATH}/src`, title: 'cwd shell' } ] }) const terminals = await runtime.listTerminals(`id:${TEST_REPO_ID}::`) expect(listWorktrees).not.toHaveBeenCalled() expect(terminals.terminals).toEqual([]) const internals = runtime as unknown as { ptysById: Map } expect(internals.ptysById.has('cwd-only-pty')).toBe(false) }) it('keeps unknown bare terminal-list ids on the no-scan exact-id path', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue( new Error('unknown explicit ids should not rescan worktrees') ) const runtime = createRuntime() await expect(runtime.listTerminals('id:not-a-repo')).resolves.toMatchObject({ terminals: [], totalCount: 0 }) expect(listWorktrees).not.toHaveBeenCalled() }) it('matches explicit-id cwd PTYs for folder workspace instance IDs', async () => { const folderWorktreeId = `${TEST_REPO_ID}::${TEST_FOLDER_WORKSPACE_PATH}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}11111111-1111-4111-8111-111111111111` vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue( new Error('folder explicit-id fallback should not rescan worktrees') ) const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'folder-cwd-pty', cwd: `${TEST_FOLDER_WORKSPACE_PATH}/src`, title: 'folder shell' } ] }) const terminals = await runtime.listTerminals(`id:${folderWorktreeId}`) expect(listWorktrees).not.toHaveBeenCalled() expect(terminals.terminals.map((terminal) => terminal.worktreeId)).toEqual([folderWorktreeId]) expect(terminals.terminals[0]?.worktreePath).toBe(TEST_FOLDER_WORKSPACE_PATH) }) it('routes PTY output through the PTY leaf index in large terminal graphs', () => { const runtime = new OrcaRuntimeService(store) const liveLeafCount = 2773 const targetIndex = liveLeafCount - 17 const tabs = Array.from({ length: liveLeafCount }, (_, index) => ({ tabId: `tab-${index}`, worktreeId: `repo-1::/tmp/worktree-${index}`, title: `Terminal ${index}`, activeLeafId: 'pane:1', layout: null })) const leaves = Array.from({ length: liveLeafCount }, (_, index) => ({ tabId: `tab-${index}`, worktreeId: `repo-1::/tmp/worktree-${index}`, leafId: 'pane:1', paneRuntimeId: 1, ptyId: `pty-${index}` })) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs, leaves }) const runtimePrivate = runtime as unknown as { leaves: Map leavesByPtyId: Map } const originalLeaves = runtimePrivate.leaves runtimePrivate.leaves = new Proxy(originalLeaves, { get(target, prop) { if ( prop === 'values' || prop === 'entries' || prop === 'keys' || prop === Symbol.iterator ) { return () => { throw new Error('onPtyData should use the PTY leaf index') } } const value = Reflect.get(target, prop, target) return typeof value === 'function' ? value.bind(target) : value } }) as Map runtime.onPtyData(`pty-${targetIndex}`, 'hello indexed\n', 123) const [targetLeaf] = runtimePrivate.leavesByPtyId.get(`pty-${targetIndex}`) ?? [] expect(targetLeaf).toMatchObject({ preview: 'hello indexed', lastOutputAt: 123 }) expect(runtime.getStatus().liveLeafCount).toBe(liveLeafCount) }) 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('resolves name selectors against worktree display names', async () => { vi.mocked(listWorktrees).mockResolvedValueOnce([ { path: TEST_WORKTREE_PATH, head: 'abc', branch: 'refs/heads/wolfiesch/orca-skill-smoke', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(store) const worktree = await runtime.showManagedWorktree('name:foo') expect(worktree).toMatchObject({ displayName: 'foo', path: TEST_WORKTREE_PATH }) }) it('routes SSH-backed forward-slash UNC file and git paths without collapsing the root', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue(new Error('local git should not run for SSH repos')) const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '//Server/Share/Repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getRepo: () => ({ id: TEST_REPO_ID, path: '//Server/Share/Repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' }) } const fsProvider = { readDir: vi.fn().mockResolvedValue([]) } const gitProvider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '//Server/Share/Repo', head: 'abc', branch: 'feature/foo', isBare: false, isMainWorktree: false } ]), getStatus: vi.fn().mockResolvedValue({ branch: 'feature/foo', files: [], ahead: 0, behind: 0, hasConflicts: false }) } registerSshFilesystemProvider('ssh-1', fsProvider as never) registerSshGitProvider('ssh-1', gitProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await runtime.readFileExplorerDir('path://server/share/repo', 'src') await runtime.getRuntimeGitStatus('path://server/share/repo') await expect(runtime.showRepo('path://server/share/repo')).resolves.toMatchObject({ path: '//Server/Share/Repo' }) } finally { unregisterSshFilesystemProvider('ssh-1') unregisterSshGitProvider('ssh-1') } expect(listWorktrees).not.toHaveBeenCalled() expect(gitProvider.listWorktrees).toHaveBeenCalledWith('//Server/Share/Repo') expect(fsProvider.readDir).toHaveBeenCalledWith('\\\\Server\\Share\\Repo\\src') expect(gitProvider.getStatus).toHaveBeenCalledWith('//Server/Share/Repo') }) it.each([ { label: 'canonical folder workspace selector', selector: TEST_FOLDER_WORKSPACE_KEY }, { label: 'id-prefixed folder workspace selector', selector: `id:${TEST_FOLDER_WORKSPACE_KEY}` } ])('reads file explorer paths for a $label', async ({ selector }) => { const folderPath = await mkdtemp(join(tmpdir(), 'orca-runtime-folder-files-')) await mkdir(join(folderPath, 'src')) await writeFile(join(folderPath, 'src', 'app.ts'), 'export {}\n') const folderWorkspace = makeFolderWorkspace({ folderPath }) const projectGroup = makeFolderProjectGroup({ parentPath: folderPath }) const runtime = new OrcaRuntimeService( createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never ) await expect(runtime.readFileExplorerDir(selector, 'src')).resolves.toContainEqual({ name: 'app.ts', isDirectory: false, isSymlink: false }) await expect(runtime.readFileExplorerPreview(selector, 'src/app.ts')).resolves.toMatchObject({ content: 'export {}\n', isBinary: false }) }) it('routes SSH folder workspace file explorer paths through the filesystem provider', async () => { const folderPath = '/srv/platform' const fsProvider = { stat: vi.fn(async (pathValue: string) => ({ size: pathValue.endsWith('/app.ts') ? 8 : 0, type: pathValue.endsWith('/app.ts') ? 'file' : 'directory', mtime: 1 })), readDir: vi.fn().mockResolvedValue([ { name: 'app.ts', isDirectory: false, isSymlink: false } ]), readFile: vi.fn().mockResolvedValue({ content: 'remote\n', isBinary: false }) } const folderWorkspace = makeFolderWorkspace({ folderPath, connectionId: 'ssh-folder' }) const projectGroup = makeFolderProjectGroup({ parentPath: folderPath }) const runtime = new OrcaRuntimeService( createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never ) registerSshFilesystemProvider('ssh-folder', fsProvider as never) try { await expect( runtime.readFileExplorerDir(`id:${TEST_FOLDER_WORKSPACE_KEY}`, 'src') ).resolves.toHaveLength(1) await expect( runtime.readFileExplorerPreview(`id:${TEST_FOLDER_WORKSPACE_KEY}`, 'src/app.ts') ).resolves.toMatchObject({ content: 'remote\n', isBinary: false }) } finally { unregisterSshFilesystemProvider('ssh-folder') } expect(fsProvider.stat).toHaveBeenCalledWith(folderPath) expect(fsProvider.readDir).toHaveBeenCalledWith('/srv/platform/src') expect(fsProvider.stat).toHaveBeenCalledWith('/srv/platform/src/app.ts') expect(fsProvider.readFile).toHaveBeenCalledWith('/srv/platform/src/app.ts') }) it('lists persisted SSH worktrees while the git provider is unavailable', async () => { vi.mocked(listWorktrees).mockClear() const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-missing' } const mainId = `${remoteRepo.id}::/home/user/repo` const childId = `${remoteRepo.id}::/home/user/repo-child` const metaById: Record = { [mainId]: makeWorktreeMeta({ displayName: 'Remote main' }), [childId]: makeWorktreeMeta({ displayName: 'Remote child', linkedPR: 42 }) } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: () => remoteRepo, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const listed = await runtime.listManagedWorktrees('id:remote-repo') expect(listWorktrees).not.toHaveBeenCalled() expect(getSshGitProviderMock).toHaveBeenCalledWith('ssh-missing') expect(listed).toMatchObject({ totalCount: 2, truncated: false, worktrees: [ { id: mainId, hostId: 'ssh:ssh-missing', path: '/home/user/repo', branch: '', isMainWorktree: true, displayName: 'Remote main' }, { id: childId, hostId: 'ssh:ssh-missing', path: '/home/user/repo-child', branch: '', isMainWorktree: false, displayName: 'Remote child', linkedPR: 42 } ] }) }) 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('does not resolve the floating-terminal sentinel as a managed worktree', async () => { const runtime = new OrcaRuntimeService(store) await expect(runtime.showManagedWorktree(FLOATING_TERMINAL_WORKTREE_ID)).rejects.toThrow( 'selector_not_found' ) await expect( runtime.showManagedWorktree(`id:${FLOATING_TERMINAL_WORKTREE_ID}`) ).rejects.toThrow('selector_not_found') }) it('guides bare repo-id worktree selectors to the full id shape', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockRejectedValue(new Error('bare repo ids should not scan worktrees')) const runtime = new OrcaRuntimeService(store) await expect(runtime.showManagedWorktree(`id:${TEST_REPO_ID}`)).rejects.toThrow( 'Worktree id selectors must use the full :: value.' ) expect(listWorktrees).not.toHaveBeenCalled() }) it('guides registered SSH repo ids without probing the provider', async () => { const remoteRepo = { ...store.getRepos()[0], connectionId: 'ssh-1' } const remoteStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined) } vi.mocked(listWorktrees).mockClear() getSshGitProviderMock.mockClear() const runtime = new OrcaRuntimeService(remoteStore) await expect(runtime.showManagedWorktree(`id:${TEST_REPO_ID}`)).rejects.toMatchObject({ code: 'worktree_id_requires_full_path' }) expect(listWorktrees).not.toHaveBeenCalled() expect(getSshGitProviderMock).not.toHaveBeenCalled() }) it('rejects bare repo ids through terminal list RPC with the structured code', async () => { vi.mocked(listWorktrees).mockClear() const runtime = new OrcaRuntimeService(store) const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS }) const response = await dispatcher.dispatch( makeRpcRequest('terminal.list', { worktree: `id:${TEST_REPO_ID}` }) ) expect(response).toMatchObject({ ok: false, error: { code: 'worktree_id_requires_full_path', message: expect.stringContaining('full :: value') } }) expect(listWorktrees).not.toHaveBeenCalled() }) it('rejects bare repo ids through the mobile session exact-id fast path', async () => { vi.mocked(listWorktrees).mockClear() const runtime = new OrcaRuntimeService(store) await expect(runtime.listMobileSessionTabs(`id:${TEST_REPO_ID}`)).rejects.toMatchObject({ code: 'worktree_id_requires_full_path' }) expect(listWorktrees).not.toHaveBeenCalled() }) it('still resolves the full worktree id selector', async () => { const runtime = new OrcaRuntimeService(store) await expect(runtime.showManagedWorktree(`id:${TEST_WORKTREE_ID}`)).resolves.toMatchObject({ id: TEST_WORKTREE_ID }) }) it('still throws selector_not_found for an unknown id selector', async () => { const runtime = new OrcaRuntimeService(store) await expect(runtime.showManagedWorktree('id:does-not-exist')).rejects.toThrow( 'selector_not_found' ) }) it('still throws selector_not_found for unknown bare ids', async () => { const runtime = new OrcaRuntimeService(store) await expect(runtime.showManagedWorktree('id:not-a-repo')).rejects.toThrow('selector_not_found') }) it('does not treat partial repo ids as full worktree ids', async () => { const runtime = new OrcaRuntimeService(store) await expect(runtime.showManagedWorktree('id:repo')).rejects.toThrow('selector_not_found') }) it('preserves full-id guidance for explicit parent-worktree lineage selectors', async () => { const runtime = new OrcaRuntimeService(store) const resolveLineage = runtime['resolveLineageForWorktreeCreate'].bind(runtime) await expect(resolveLineage({ parentWorktree: `id:${TEST_REPO_ID}` })).rejects.toThrow( 'Worktree id selectors must use the full :: value.' ) }) it('does not reuse stale in-flight worktree scans after creating a worktree', async () => { const runtime = new OrcaRuntimeService(store) const staleScan = deferred() const createdWorktree = { path: '/tmp/workspaces/cache-race', head: 'def', branch: 'cache-race', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(listWorktrees) .mockImplementationOnce(() => staleScan.promise) .mockResolvedValueOnce([createdWorktree]) .mockResolvedValueOnce([...MOCK_GIT_WORKTREES, createdWorktree]) const staleLookup = runtime.showManagedWorktree(TEST_WORKTREE_ID) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'cache-race' }) const freshLookup = runtime.showManagedWorktree(result.worktree.id) staleScan.resolve(MOCK_GIT_WORKTREES) await expect(staleLookup).resolves.toMatchObject({ id: TEST_WORKTREE_ID }) await expect(freshLookup).resolves.toMatchObject({ id: result.worktree.id, path: createdWorktree.path }) await expect(runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)).resolves.toMatchObject( { worktrees: expect.arrayContaining([expect.objectContaining({ path: createdWorktree.path })]) } ) }) it('creates additional workspace metadata for folder-mode repos through runtime create', async () => { const folderRepo = { id: 'folder-repo', path: '/workspace/folder', displayName: 'Folder', badgeColor: 'blue', addedAt: 1, kind: 'folder' as const } const rootWorktreeId = 'folder-repo::/workspace/folder' const rootPriorWorktreeIds = ['folder-repo::/workspace/old-folder'] const metaById: Record = { [rootWorktreeId]: makeWorktreeMeta({ instanceId: 'root-instance', priorWorktreeIds: rootPriorWorktreeIds }) } const runtimeStore = { ...store, getRepos: () => [folderRepo], getRepo: (id: string) => (id === folderRepo.id ? folderRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, removeWorktreeMeta: (worktreeId: string) => { delete metaById[worktreeId] } } let deletedWorktreeId = '' const localProvider = { listProcesses: vi.fn(async () => [{ id: `${deletedWorktreeId}@@pty-1` }]), shutdown: vi.fn(async () => undefined) } const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getLocalProvider: () => localProvider as never }) const notifier = { worktreesChanged: vi.fn() } runtime.setNotifier(notifier as never) const result = await runtime.createManagedWorktree({ repoSelector: 'id:folder-repo', name: 'folder-session', createdWithAgent: 'codex' }) expect(addWorktreeMock).not.toHaveBeenCalled() expect(result.worktree).toEqual( expect.objectContaining({ id: expect.stringMatching(/^folder-repo::\/workspace\/folder::workspace:[0-9a-f-]{36}$/), repoId: 'folder-repo', path: '/workspace/folder', displayName: 'folder-session', isMainWorktree: false, createdWithAgent: 'codex' }) ) expect(metaById[result.worktree.id]).toMatchObject({ instanceId: result.worktree.instanceId, displayName: 'folder-session', orcaCreationSource: 'runtime', createdWithAgent: 'codex' }) await expect(runtime.showManagedWorktree(`id:${result.worktree.id}`)).resolves.toMatchObject({ id: result.worktree.id, repoId: 'folder-repo', path: '/workspace/folder', displayName: 'folder-session' }) await expect(runtime.listManagedWorktrees('id:folder-repo')).resolves.toMatchObject({ totalCount: 2, worktrees: [ expect.objectContaining({ id: rootWorktreeId, isMainWorktree: true, priorWorktreeIds: rootPriorWorktreeIds }), expect.objectContaining({ id: result.worktree.id, isMainWorktree: false }) ] }) await expect( runtime.updateManagedWorktreeMeta(`id:${result.worktree.id}`, { comment: 'note' }) ).resolves.toMatchObject({ id: result.worktree.id, comment: 'note' }) await expect( runtime.removeManagedWorktree('id:folder-repo::/workspace/folder') ).rejects.toThrow('Cannot delete the project root workspace') deletedWorktreeId = result.worktree.id await expect(runtime.removeManagedWorktree(`id:${result.worktree.id}`)).resolves.toEqual({}) expect(localProvider.shutdown).toHaveBeenCalledWith( `${result.worktree.id}@@pty-1`, expect.objectContaining({ immediate: true }) ) expect(metaById[result.worktree.id]).toBeUndefined() expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(result.worktree.id) expect(notifier.worktreesChanged).toHaveBeenCalledWith('folder-repo') }) it('refreshes runtime remote-tracking bases before creating local worktrees', async () => { const runtime = new OrcaRuntimeService(store) const refresh = deferred<{ stdout: string; stderr: string }>() const createdWorktree = { path: '/tmp/workspaces/cli-fresh-base', head: 'def', branch: 'cli-fresh-base', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/cli-fresh-base^{commit}')) { throw new Error('branch not found') } if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } if (args[0] === 'rev-parse' && args[1] === '--verify') { return { stdout: 'base-sha\n', stderr: '' } } if (args.includes('fetch')) { return refresh.promise } return { stdout: '', stderr: '' } }) try { const createPromise = runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'cli-fresh-base' }) await vi.waitFor(() => { expect(gitSpy).toHaveBeenCalledWith( [ '-c', 'maintenance.auto=false', '-c', 'maintenance.commit-graph.auto=0', '-c', 'gc.auto=0', 'fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main' ], { cwd: TEST_REPO_PATH, useConfiguredSshCommandForNetwork: true, timeout: 60_000 } ) }) expect(addWorktree).not.toHaveBeenCalled() refresh.resolve({ stdout: '', stderr: '' }) const result = await createPromise expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'cli-fresh-base', 'origin/main', false, false, { suggestLocalBaseRefUpdate: true, remoteTrackingBase: { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } } ) expect(result.worktree).toMatchObject({ path: createdWorktree.path, baseRef: 'refs/remotes/origin/main' }) } finally { gitSpy.mockRestore() } }) it('returns runtime local base update suggestions from addWorktree', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/cli-stale-main', head: 'def', branch: 'cli-stale-main', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(addWorktree).mockResolvedValueOnce({ localBaseRefUpdateSuggestion: { baseRef: 'origin/main', localBranch: 'main', behind: 5 } }) vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/cli-stale-main^{commit}')) { throw new Error('branch not found') } if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } if (args[0] === 'rev-parse' && args[1] === '--verify') { return { stdout: 'base-sha\n', stderr: '' } } return { stdout: '', stderr: '' } }) try { const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'cli-stale-main' }) expect(result.localBaseRefUpdateSuggestion).toEqual({ baseRef: 'origin/main', localBranch: 'main', behind: 5 }) } finally { gitSpy.mockRestore() } }) it('creates a runtime local worktree from the detected default when the persisted base is stale', async () => { // Regression: a stale persisted repo base must fall back to the detected default. const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/cli-refresh-fails', head: 'base-sha', branch: 'cli-refresh-fails', isBare: false, isMainWorktree: false } const repo = { ...store.getRepos()[0], worktreeBaseRef: 'origin/master' } const getReposSpy = vi.spyOn(store, 'getRepos').mockReturnValue([repo] as never) computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(addWorktree).mockResolvedValueOnce({}) vi.mocked(listWorktrees).mockResolvedValue([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/master^{commit}')) { throw new Error('missing ref') } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { return { stdout: 'base-sha\n', stderr: '' } } if (args.includes('fetch')) { throw new Error('network unavailable') } return { stdout: '', stderr: '' } }) try { await expect( runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'cli-refresh-fails' }) ).resolves.toBeDefined() expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'cli-refresh-fails', 'origin/main', false, false, { remoteTrackingBase: { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' }, suggestLocalBaseRefUpdate: true } ) expect(getBaseRefDefault).toHaveBeenCalled() } finally { getReposSpy.mockRestore() gitSpy.mockRestore() } }) it('creates a runtime local worktree from a usable persisted local branch base', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/local-branch-base', head: 'develop-sha', branch: 'local-branch-base', isBare: false, isMainWorktree: false } const repo = { ...store.getRepos()[0], worktreeBaseRef: 'develop' } const getReposSpy = vi.spyOn(store, 'getRepos').mockReturnValue([repo] as never) computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(addWorktree).mockResolvedValueOnce({}) vi.mocked(listWorktrees).mockResolvedValue([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/develop^{commit}')) { return { stdout: 'develop-sha\n', stderr: '' } } if (args.includes('fetch')) { throw new Error('network unavailable') } return { stdout: '', stderr: '' } }) try { await expect( runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'local-branch-base' }) ).resolves.toBeDefined() expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'local-branch-base', 'develop', false ) } finally { getReposSpy.mockRestore() gitSpy.mockRestore() } }) it('creates a runtime local worktree from a slash-named local branch matching a remote prefix', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/slash-local-base', head: 'team-feature-sha', branch: 'slash-local-base', isBare: false, isMainWorktree: false } const repo = { ...store.getRepos()[0], worktreeBaseRef: 'team/feature' } const getReposSpy = vi.spyOn(store, 'getRepos').mockReturnValue([repo] as never) computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(addWorktree).mockResolvedValueOnce({}) vi.mocked(listWorktrees).mockResolvedValue([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'remote') { return { stdout: 'team\norigin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { throw new Error('missing remote-tracking ref') } if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { return { stdout: 'team-feature-sha\n', stderr: '' } } if (args.includes('fetch')) { throw new Error('network unavailable') } return { stdout: '', stderr: '' } }) try { await expect( runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'slash-local-base' }) ).resolves.toBeDefined() expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'slash-local-base', 'team/feature', false ) expect(gitSpy).not.toHaveBeenCalledWith( [ '-c', 'maintenance.auto=false', '-c', 'maintenance.commit-graph.auto=0', '-c', 'gc.auto=0', 'fetch', '--no-tags', 'team', '+refs/heads/feature:refs/remotes/team/feature' ], expect.any(Object) ) } finally { getReposSpy.mockRestore() gitSpy.mockRestore() } }) it('does not create a runtime local worktree when the refresh fails and no local base ref exists', async () => { const runtime = new OrcaRuntimeService(store) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/cli-refresh-no-local') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/cli-refresh-no-local') const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } // No local remote-tracking base ref -> nothing to fall back on. if (args[0] === 'rev-parse' && args[1] === '--verify') { throw new Error('missing ref') } if (args.includes('fetch')) { throw new Error('network unavailable') } return { stdout: '', stderr: '' } }) try { await expect( runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'cli-refresh-no-local' }) ).rejects.toThrow( 'Could not refresh base ref "origin/main" from "origin". Check your network and try again.' ) expect(addWorktree).not.toHaveBeenCalled() } finally { gitSpy.mockRestore() } }) it('creates a branchNameOverride worktree from the selected matching remote base ref', async () => { const runtime = new OrcaRuntimeService(store) vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/feature-something') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/feature-something') vi.mocked(listWorktrees).mockResolvedValueOnce([ { path: '/tmp/workspaces/feature-something', head: 'def', branch: 'feature/something', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'feature/something', baseBranch: 'origin/feature/something', branchNameOverride: 'feature/something' }) expect(getBranchConflictKind).toHaveBeenCalledWith( TEST_REPO_PATH, 'feature/something', 'origin/feature/something' ) expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, '/tmp/workspaces/feature-something', 'feature/something', 'origin/feature/something', false ) expect(resolveLocalGitUsernameMock).not.toHaveBeenCalled() expect(result.worktree).toMatchObject({ path: '/tmp/workspaces/feature-something', branch: 'feature/something' }) }) it('checks out a selected existing local branch even when that branch already has a PR', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-bug-0', head: 'def', branch: 'refs/heads/fix/bug-0', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(getBranchConflictKind).mockClear() getPRForBranchMock.mockResolvedValue({ number: 42, title: 'Existing PR', state: 'open', url: 'https://example.com/pr/42', checksStatus: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: TEST_REPO_PATH, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args[1] === '--verify') { return { stdout: 'branch-sha\n', stderr: '' } } return { stdout: '', stderr: '' } }) try { const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix/bug-0', baseBranch: 'fix/bug-0', branchNameOverride: 'fix/bug-0' }) expect(getBranchConflictKind).not.toHaveBeenCalled() expect(getPRForBranchMock).not.toHaveBeenCalled() expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'fix/bug-0', 'fix/bug-0', false, false, { checkoutExistingBranch: true } ) expect(result.worktree).toMatchObject({ path: createdWorktree.path, branch: 'refs/heads/fix/bug-0' }) } finally { gitSpy.mockRestore() } }) it('creates a same-repo PR branch override from a resolved head SHA and matching push target', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-title', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(getBranchConflictKind).mockResolvedValueOnce('remote') vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) getPRForBranchMock.mockResolvedValueOnce({ number: 42, title: 'Selected PR', state: 'open', url: 'https://example.com/pr/42', checksStatus: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) try { const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: 42, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(getBranchConflictKind).toHaveBeenCalledWith(TEST_REPO_PATH, 'feature/fix', 'abc123') expect(getPRForBranchMock).toHaveBeenCalledWith(TEST_REPO_PATH, 'feature/fix') expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix', 'abc123', false ) expect(gitSpy).toHaveBeenCalledWith( ['branch', '--set-upstream-to', 'origin/feature/fix', 'feature/fix'], { cwd: createdWorktree.path } ) expect(result.worktree).toMatchObject({ path: createdWorktree.path, branch: 'refs/heads/feature/fix' }) } finally { gitSpy.mockRestore() } }) it('skips broad remote fetch for an existing full-SHA PR base', async () => { const runtime = new OrcaRuntimeService(store) const sha = 'c'.repeat(40) const createdWorktree = { path: '/tmp/workspaces/fix-title', head: sha, branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(getBranchConflictKind).mockResolvedValueOnce(null) vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { throw new Error('branch not found') } if (args[0] === 'rev-parse' && args.includes(`${sha}^{commit}`)) { return { stdout: `${sha}\n`, stderr: '' } } return { stdout: '', stderr: '' } }) try { const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: sha, branchNameOverride: 'feature/fix' }) expect(gitSpy).not.toHaveBeenCalledWith(['fetch', 'origin'], expect.anything()) expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix', sha, false ) expect(result.worktree).toMatchObject({ path: createdWorktree.path, branch: 'refs/heads/feature/fix' }) } finally { gitSpy.mockRestore() } }) it('creates a selected Bitbucket PR branch override from a matching remote branch', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/bitbucket-title', head: 'abc123', branch: 'refs/heads/feature/bitbucket', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(getBranchConflictKind).mockResolvedValueOnce('remote') vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) getHostedReviewForBranchMock.mockResolvedValueOnce({ provider: 'bitbucket', number: 11, title: 'Bitbucket PR', state: 'open', url: 'https://bitbucket.org/team/repo/pull-requests/11', status: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) try { const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'bitbucket-title', baseBranch: 'abc123', branchNameOverride: 'feature/bitbucket', linkedBitbucketPR: 11, pushTarget: { remoteName: 'origin', branchName: 'feature/bitbucket' } }) expect(getBranchConflictKind).toHaveBeenCalledWith( TEST_REPO_PATH, 'feature/bitbucket', 'abc123' ) expect(getHostedReviewForBranchMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: TEST_REPO_PATH, branch: 'feature/bitbucket', linkedBitbucketPR: 11 }) ) expect(getPRForBranchMock).not.toHaveBeenCalled() expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/bitbucket', 'abc123', false ) expect(result.worktree).toMatchObject({ path: createdWorktree.path, branch: 'refs/heads/feature/bitbucket', linkedBitbucketPR: 11 }) } finally { gitSpy.mockRestore() } }) it('suffixes an existing PR when a matching push target lacks selected PR metadata', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockImplementation( (sanitizedName: string) => `/tmp/workspaces/${sanitizedName}` ) ensurePathWithinWorkspaceMock.mockImplementation((pathValue: string) => pathValue) vi.mocked(getBranchConflictKind).mockResolvedValueOnce(null) vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) getPRForBranchMock.mockResolvedValueOnce({ number: 42, title: 'Existing PR', state: 'open', url: 'https://example.com/pr/42', checksStatus: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix-2^{commit}')) { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }) try { await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(getPRForBranchMock).toHaveBeenCalledWith(TEST_REPO_PATH, 'feature/fix') expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix-2', 'abc123', false ) } finally { gitSpy.mockRestore() } }) it('suffixes a matching push target branch when selected PR metadata has no PR number', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockImplementation( (sanitizedName: string) => `/tmp/workspaces/${sanitizedName}` ) ensurePathWithinWorkspaceMock.mockImplementation((pathValue: string) => pathValue) vi.mocked(getBranchConflictKind).mockResolvedValueOnce('remote') vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix-2^{commit}')) { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }) try { await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: null, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix-2', 'abc123', false ) } finally { gitSpy.mockRestore() } }) it('suffixes a matching push target branch when the existing PR is different', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockImplementation( (sanitizedName: string) => `/tmp/workspaces/${sanitizedName}` ) ensurePathWithinWorkspaceMock.mockImplementation((pathValue: string) => pathValue) vi.mocked(getBranchConflictKind).mockResolvedValueOnce('remote') vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) getPRForBranchMock.mockResolvedValueOnce({ number: 43, title: 'Different PR', state: 'open', url: 'https://example.com/pr/43', checksStatus: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix-2^{commit}')) { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }) try { await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: 42, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(getPRForBranchMock).toHaveBeenCalledWith(TEST_REPO_PATH, 'feature/fix') expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix-2', 'abc123', false ) } finally { gitSpy.mockRestore() } }) it('suffixes a selected PR remote conflict when the PR lookup fails', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockImplementation( (sanitizedName: string) => `/tmp/workspaces/${sanitizedName}` ) ensurePathWithinWorkspaceMock.mockImplementation((pathValue: string) => pathValue) vi.mocked(getBranchConflictKind).mockResolvedValueOnce('remote') vi.mocked(listWorktrees).mockResolvedValueOnce([createdWorktree]) getPRForBranchMock.mockRejectedValueOnce(new Error('gh unavailable')) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix-2^{commit}')) { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }) try { await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: 42, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(getPRForBranchMock).toHaveBeenCalledWith(TEST_REPO_PATH, 'feature/fix') expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix-2', 'abc123', false ) } finally { gitSpy.mockRestore() } }) it('checks out an unused runtime PR branch only when it is at the resolved head SHA', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-title', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(getBranchConflictKind).mockClear() vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: TEST_REPO_PATH, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('abc123^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }) try { await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix' }) expect(getBranchConflictKind).not.toHaveBeenCalled() expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix', 'abc123', false, false, { checkoutExistingBranch: true } ) } finally { gitSpy.mockRestore() } }) it('suffixes only the runtime worktree path when an exact PR branch checkout path exists', async () => { const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockImplementation((sanitizedName: string) => sanitizedName === 'fix-title' ? process.cwd() : `/tmp/workspaces/${sanitizedName}` ) ensurePathWithinWorkspaceMock.mockImplementation((pathValue: string) => pathValue) vi.mocked(getBranchConflictKind).mockClear() vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: TEST_REPO_PATH, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: TEST_REPO_PATH, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('abc123^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }) try { await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix' }) expect(getBranchConflictKind).not.toHaveBeenCalled() expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'feature/fix', 'abc123', false, false, { checkoutExistingBranch: true } ) } finally { gitSpy.mockRestore() } }) it('rejects when every exact PR branch checkout path suffix is occupied', async () => { const runtime = new OrcaRuntimeService(store) computeWorktreePathMock.mockReturnValue(process.cwd()) ensurePathWithinWorkspaceMock.mockImplementation((pathValue: string) => pathValue) vi.mocked(getBranchConflictKind).mockResolvedValueOnce(null) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }) try { await expect( runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix' }) ).rejects.toThrow( 'Could not find an available worktree path for "fix-title". Pick a different worktree name.' ) expect(addWorktree).not.toHaveBeenCalled() } finally { gitSpy.mockRestore() } }) it('creates SSH-backed worktrees through the SSH provider for mobile/runtime callers', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(addWorktree).mockClear() const created = { path: '/remote/mobile-feature', head: 'def', branch: 'refs/heads/mobile-feature', isBare: false, isMainWorktree: false } const metaById: Record = {} const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } registerSshGitProvider('ssh-1', provider as never) getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) const runtime = new OrcaRuntimeService(remoteStore as never) const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'mobile-feature', linkedGitLabIssue: 321, linkedGitLabMR: 654, startup: { command: 'claude' } }) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'mobile-feature', '/remote/mobile-feature', { base: 'origin/main' } ) expect(result.worktree).toMatchObject({ id: `${TEST_REPO_ID}::${created.path}`, path: created.path, linkedGitLabIssue: 321, linkedGitLabMR: 654 }) expect(metaById[result.worktree.id]).toMatchObject({ linkedGitLabIssue: 321, linkedGitLabMR: 654 }) expect(addWorktree).not.toHaveBeenCalled() expect(listWorktrees).not.toHaveBeenCalled() }) it('records lineage for SSH-backed CLI-created worktrees', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(addWorktree).mockClear() const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1', hookSettings: { mode: 'auto' as const, setupRunPolicy: 'run-by-default' as const, setupAgentStartupPolicy: 'wait-for-setup' as const, scripts: { setup: '', archive: '' } } } const parent = { path: '/remote/repo-parent', head: 'abc', branch: 'refs/heads/repo-parent', isBare: false, isMainWorktree: false } const created = { path: '/remote/child-feature', head: 'def', branch: 'refs/heads/child-feature', isBare: false, isMainWorktree: false } const parentId = `${TEST_REPO_ID}::${parent.path}` const childId = `${TEST_REPO_ID}::${created.path}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }) } const lineageById: Record = {} const remoteStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, getWorktreeLineage: (worktreeId: string) => lineageById[worktreeId], setWorktreeLineage: vi.fn((worktreeId: string, lineage: WorktreeLineage) => { lineageById[worktreeId] = lineage return lineage }) } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValueOnce([parent]).mockResolvedValue([parent, created]) } registerSshGitProvider('ssh-1', provider as never) getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) const runtime = new OrcaRuntimeService(remoteStore as never) try { const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'child-feature', lineage: { parentWorktree: `id:${parentId}` } }) expect(result.worktree).toMatchObject({ id: childId, parentWorktreeId: parentId, lineage: expect.objectContaining({ worktreeId: childId, parentWorktreeId: parentId, worktreeInstanceId: metaById[childId].instanceId, parentWorktreeInstanceId: 'parent-instance', origin: 'cli' }) }) expect(result.lineage).toBe(result.worktree.lineage) expect(result.warnings).toEqual([]) expect(remoteStore.setWorktreeLineage).toHaveBeenCalledWith(childId, expect.any(Object)) expect(addWorktree).not.toHaveBeenCalled() expect(listWorktrees).not.toHaveBeenCalled() } finally { unregisterSshGitProvider('ssh-1') } }) it('records folder workspace lineage inferred from environment context', async () => { vi.mocked(addWorktree).mockClear() const created = { path: '/tmp/workspaces/folder-child', head: 'def', branch: 'refs/heads/folder-child', isBare: false, isMainWorktree: false } const childId = `${TEST_REPO_ID}::${created.path}` const metaById: Record = {} const workspaceLineageByChildKey: Record = {} const runtimeStore = { ...createFolderWorkspaceRuntimeStore(), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, setWorkspaceLineage: vi.fn((lineage: WorkspaceLineage) => { workspaceLineageByChildKey[lineage.childWorkspaceKey] = lineage return lineage }) } computeWorktreePathMock.mockReturnValue(created.path) ensurePathWithinWorkspaceMock.mockImplementation((pathValue: string) => pathValue) vi.mocked(listWorktrees).mockResolvedValueOnce([created]) const runtime = new OrcaRuntimeService(runtimeStore as never) const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'folder-child', baseBranch: 'origin/main', lineage: { envParentWorkspace: TEST_FOLDER_WORKSPACE_KEY } }) expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, created.path, 'folder-child', 'origin/main', false ) expect(result.lineage).toBeNull() expect(result.workspaceLineage).toMatchObject({ childWorkspaceKey: `worktree:${childId}`, childInstanceId: metaById[childId].instanceId, parentWorkspaceKey: TEST_FOLDER_WORKSPACE_KEY, parentInstanceId: null, origin: 'cli', capture: { source: 'env-workspace', confidence: 'inferred' } }) expect(result.worktree.workspaceLineage).toBe(result.workspaceLineage) expect(runtimeStore.setWorkspaceLineage).toHaveBeenCalledWith( expect.objectContaining({ childWorkspaceKey: `worktree:${childId}`, parentWorkspaceKey: TEST_FOLDER_WORKSPACE_KEY }) ) }) it('activates SSH worktrees created with startup agents', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(addWorktree).mockClear() const created = { path: '/remote/agent-feature', head: 'def', branch: 'refs/heads/agent-feature', isBare: false, isMainWorktree: false } const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1', hookSettings: { mode: 'auto' as const, setupRunPolicy: 'run-by-default' as const, setupAgentStartupPolicy: 'wait-for-setup' as const, scripts: { setup: '', archive: '' } } } const metaById: Record = {} const remoteStore = { ...store, getSettings: () => ({ ...store.getSettings(), terminalWindowsShell: 'cmd.exe', agentCmdOverrides: {} }), getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } const spawn = vi.fn().mockResolvedValue({ id: 'pty-remote-agent-startup' }) const activateWorktree = vi.fn() registerSshGitProvider('ssh-1', provider as never) getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-remote-agent-startup' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) try { const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'agent-feature', startupAgent: 'codex', startupPrompt: 'hi', activate: true }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/remote/agent-feature', command: "codex '--dangerously-bypass-approvals-and-sandbox' 'hi'", worktreeId: result.worktree.id }) ) expect(activateWorktree).toHaveBeenCalledWith( TEST_REPO_ID, result.worktree.id, undefined, undefined, undefined ) expect(addWorktree).not.toHaveBeenCalled() expect(listWorktrees).not.toHaveBeenCalled() } finally { unregisterSshGitProvider('ssh-1') } }) it('quotes startup prompts for Windows SSH worktrees using PowerShell syntax', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(addWorktree).mockClear() const created = { path: 'C:/remote/agent-feature', head: 'def', branch: 'refs/heads/agent-feature', isBare: false, isMainWorktree: false } const remoteRepo = { id: TEST_REPO_ID, path: 'C:/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const metaById: Record = {} const remoteStore = { ...store, getSettings: () => ({ ...store.getSettings(), agentCmdOverrides: {} }), getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } const spawn = vi.fn().mockResolvedValue({ id: 'pty-remote-windows-agent' }) registerSshGitProvider('ssh-1', provider as never) getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-remote-windows-agent' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) try { await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'agent-feature', startupAgent: 'codex', startupPrompt: "fix Bob's branch" }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: 'C:/remote/agent-feature', command: "codex '--dangerously-bypass-approvals-and-sandbox' 'fix Bob''s branch'" }) ) expect(addWorktree).not.toHaveBeenCalled() expect(listWorktrees).not.toHaveBeenCalled() } finally { unregisterSshGitProvider('ssh-1') } }) it('launches SSH setup terminals for runtime task-created worktrees', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(addWorktree).mockClear() const created = { path: '/remote/mobile-setup', head: 'def', branch: 'refs/heads/mobile-setup', isBare: false, isMainWorktree: false } const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1', hookSettings: { mode: 'auto' as const, setupRunPolicy: 'run-by-default' as const, setupAgentStartupPolicy: 'wait-for-setup' as const, scripts: { setup: '', archive: '' } } } const metaById: Record = {} const remoteStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } if (args[0] === 'rev-parse' && args[1] === '--git-path') { return { stdout: '/remote/repo/.git/worktrees/mobile-setup/orca/setup-runner.sh\n', stderr: '' } } if (args[0] === 'rev-parse') { throw new Error('missing local branch') } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } const fsProvider = { readFile: vi.fn().mockResolvedValue({ isBinary: false, content: 'hooks:\n' }), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } vi.mocked(getEffectiveHooksFromConfig).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-remote-agent' }) .mockResolvedValueOnce({ id: 'pty-remote-setup' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-remote' }) registerSshGitProvider('ssh-1', provider as never) registerSshFilesystemProvider('ssh-1', fsProvider as never) getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) try { const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'mobile-setup', setupDecision: 'run', startup: { command: 'claude' } }) // Why: runtime provisions setup itself (fire-and-forget) and omits it from the RPC result so the caller doesn't double-spawn. expect(result.setup).toBeUndefined() await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) expect(spawn).toHaveBeenNthCalledWith( 1, expect.objectContaining({ cwd: '/remote/mobile-setup', command: expect.stringContaining('exec claude'), worktreeId: result.worktree.id }) ) expect(spawn).toHaveBeenNthCalledWith( 2, expect.objectContaining({ cwd: '/remote/mobile-setup', command: expect.stringContaining( 'bash /remote/repo/.git/worktrees/mobile-setup/orca/setup-runner.sh' ), worktreeId: result.worktree.id }) ) const startupCommand = (spawn.mock.calls[0]![0] as { command: string }).command const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command const nonceMatch = startupCommand.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() const markerPath = `/remote/repo/.git/worktrees/mobile-setup/orca/setup-runner.sh.${nonceMatch![1]}.done` expect(setupCommand).toContain('printf') expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) expect(startupCommand).toContain(markerPath) expect(setupCommand).toContain(markerPath) expect(revealTerminalSession).toHaveBeenLastCalledWith( result.worktree.id, expect.objectContaining({ ptyId: 'pty-remote-setup', title: 'Setup', activate: false }) ) } finally { unregisterSshGitProvider('ssh-1') unregisterSshFilesystemProvider('ssh-1') } }) it('honors split setup placement for SSH worktrees without startup agents', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(addWorktree).mockClear() const created = { path: '/remote/mobile-setup-split', head: 'def', branch: 'refs/heads/mobile-setup-split', isBare: false, isMainWorktree: false } const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const metaById: Record = {} const remoteStore = { ...store, getSettings: () => ({ ...store.getSettings(), setupScriptLaunchMode: 'split-horizontal' as const }), getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } if (args[0] === 'rev-parse' && args[1] === '--git-path') { return { stdout: '/remote/repo/.git/worktrees/mobile-setup-split/orca/setup-runner.sh\n', stderr: '' } } if (args[0] === 'rev-parse') { throw new Error('missing local branch') } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } const fsProvider = { readFile: vi.fn().mockResolvedValue({ isBinary: false, content: 'hooks:\n' }), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } vi.mocked(getEffectiveHooksFromConfig).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-remote-initial' }) .mockResolvedValueOnce({ id: 'pty-remote-setup-split' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-remote-split' }) registerSshGitProvider('ssh-1', provider as never) registerSshFilesystemProvider('ssh-1', fsProvider as never) getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) try { const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'mobile-setup-split', setupDecision: 'run' }) await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) const initialEnv = (spawn.mock.calls[0]![0] as { env?: Record }).env ?? {} const setupEnv = (spawn.mock.calls[1]![0] as { env?: Record }).env ?? {} expect(setupEnv.ORCA_TAB_ID).toBe(initialEnv.ORCA_TAB_ID) const initialLeafId = initialEnv.ORCA_PANE_KEY!.slice(`${initialEnv.ORCA_TAB_ID!}:`.length) expect(revealTerminalSession).toHaveBeenLastCalledWith( result.worktree.id, expect.objectContaining({ ptyId: 'pty-remote-setup-split', tabId: initialEnv.ORCA_TAB_ID, activate: false, splitFromLeafId: initialLeafId, splitDirection: 'horizontal' }) ) } finally { unregisterSshGitProvider('ssh-1') unregisterSshFilesystemProvider('ssh-1') } }) it('removes SSH-backed runtime worktrees through the SSH git provider', async () => { vi.mocked(listWorktrees).mockClear() const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getRepo: () => ({ id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' }) } const gitProvider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/remote/feature', head: 'abc', branch: 'feature/foo', isBare: false, isMainWorktree: false } ]), removeWorktree: vi.fn().mockResolvedValue(undefined) } registerSshGitProvider('ssh-1', gitProvider as never) const ptyProvider = { listProcesses: vi.fn().mockResolvedValue([ { id: 'pty-remote', cwd: '/remote/feature', title: 'shell', worktreeId: `${TEST_REPO_ID}::/remote/feature` } ]), shutdown: vi.fn().mockResolvedValue(undefined) } const runtime = new OrcaRuntimeService(remoteStore as never, undefined, { getSshProvider: () => ptyProvider as never }) try { await runtime.removeManagedWorktree('path:/remote/feature', true, false) } finally { unregisterSshGitProvider('ssh-1') } expect(gitProvider.removeWorktree).toHaveBeenCalledWith('/remote/feature', true) expect(ptyProvider.shutdown).toHaveBeenCalledWith( 'pty-remote', expect.objectContaining({ immediate: true }) ) expect(ptyProvider.shutdown.mock.invocationCallOrder[0]).toBeLessThan( gitProvider.removeWorktree.mock.invocationCallOrder[0] ) expect(closeRemoteWatcherForWorktreePathMock).toHaveBeenCalledWith('ssh-1', '/remote/feature') expect(closeRemoteWatcherForWorktreePathMock.mock.invocationCallOrder[0]).toBeLessThan( gitProvider.removeWorktree.mock.invocationCallOrder[0] ) expect(removeWorktree).not.toHaveBeenCalled() expect(listWorktrees).not.toHaveBeenCalled() expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(`${TEST_REPO_ID}::/remote/feature`) }) it('rejects SSH-backed runtime removal of the main worktree before provider deletion', async () => { const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getRepo: () => ({ id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' }) } const gitProvider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]), removeWorktree: vi.fn().mockResolvedValue(undefined) } registerSshGitProvider('ssh-1', gitProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await expect(runtime.removeManagedWorktree('path:/remote/repo', true)).rejects.toThrow( 'Refusing to delete protected worktree path: /remote/repo' ) } finally { unregisterSshGitProvider('ssh-1') } expect(gitProvider.removeWorktree).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() }) it('reads SSH repo hooks through the SSH filesystem provider', async () => { const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: 'C:/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ] } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n setup: pnpm install\n', isBinary: false }) } vi.mocked(parseOrcaYaml).mockReturnValue({ scripts: { setup: 'pnpm install' } }) registerSshFilesystemProvider('ssh-1', fsProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await expect(runtime.getRepoHooks('id:repo-1')).resolves.toMatchObject({ hasHooksFile: true, hooks: { scripts: { setup: 'pnpm install' } }, source: 'orca.yaml', setupTrust: { contentHash: '005d0b7e5c261dcc5e2f8568e69a0b30e889a3275b55b18ec20a7deef0081e90', scriptContent: 'pnpm install' } }) } finally { unregisterSshFilesystemProvider('ssh-1') } expect(fsProvider.readFile).toHaveBeenCalledWith('C:\\remote\\repo\\orca.yaml') expect(hasHooksFile).not.toHaveBeenCalled() expect(getEffectiveHooks).not.toHaveBeenCalled() }) it('hashes only the shared orca.yaml setup script for local run-both hooks', async () => { vi.mocked(hasHooksFile).mockReturnValue(true) vi.mocked(loadHooks).mockReturnValue({ scripts: { setup: 'echo yaml setup' } }) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'echo yaml setup\necho local setup' } }) const runtimeStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1, hookSettings: { commandSourcePolicy: 'run-both' as const, scripts: { setup: 'echo local setup' } } } ] } const runtime = new OrcaRuntimeService(runtimeStore as never) await expect(runtime.getRepoHooks('id:repo-1')).resolves.toMatchObject({ hooks: { scripts: { setup: 'echo yaml setup\necho local setup' } }, setupTrust: { contentHash: '9bc9f57699fe0390d263cca1aec01235cccc8fa5fc87cd87fd51ba1c8483ec84', scriptContent: 'echo yaml setup' } }) }) it('uses remote path joins for SSH hook checks and issue-command files', async () => { const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: 'C:/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ] } const fsProvider = { readFile: vi.fn(async (filePath: string) => ({ content: filePath.endsWith('orca.yaml') ? 'scripts:\n setup: pnpm install\n' : filePath.endsWith('.gitignore') ? 'node_modules\n' : 'Fix it', isBinary: false })), writeFile: vi.fn().mockResolvedValue(undefined), createDir: vi.fn().mockResolvedValue(undefined), deletePath: vi.fn().mockResolvedValue(undefined) } registerSshFilesystemProvider('ssh-1', fsProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await expect(runtime.checkRepoHooks('id:repo-1')).resolves.toMatchObject({ hasHooks: true, mayNeedUpdate: false }) await expect(runtime.readRepoIssueCommand('id:repo-1')).resolves.toMatchObject({ localContent: 'Fix it', effectiveContent: 'Fix it', localFilePath: 'C:\\remote\\repo\\.orca\\issue-command' }) await expect(runtime.writeRepoIssueCommand('id:repo-1', 'Ship it')).resolves.toEqual({ ok: true }) } finally { unregisterSshFilesystemProvider('ssh-1') } expect(fsProvider.readFile).toHaveBeenCalledWith('C:\\remote\\repo\\orca.yaml') expect(fsProvider.readFile).toHaveBeenCalledWith('C:\\remote\\repo\\.orca\\issue-command') expect(fsProvider.createDir).toHaveBeenCalledWith('C:\\remote\\repo\\.orca') expect(fsProvider.writeFile).toHaveBeenCalledWith( 'C:\\remote\\repo\\.orca\\issue-command', 'Ship it\n' ) expect(fsProvider.writeFile).toHaveBeenCalledWith( 'C:\\remote\\repo\\.gitignore', 'node_modules\n.orca\n' ) }) it('resolves SSH issue commands from shared orca.yaml and deletes empty overrides', async () => { const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ] } vi.mocked(parseOrcaYaml).mockReturnValue({ scripts: {}, issueCommand: 'claude -p "Fix #{{issue}}"' }) const fsProvider = { readFile: vi.fn(async (filePath: string) => { if (filePath.endsWith('.orca/issue-command')) { throw Object.assign(new Error('missing'), { code: 'ENOENT' }) } if (filePath.endsWith('orca.yaml')) { return { content: 'issueCommand: claude -p "Fix #{{issue}}"', isBinary: false } } return { content: '', isBinary: false } }), writeFile: vi.fn().mockResolvedValue(undefined), createDir: vi.fn().mockResolvedValue(undefined), deletePath: vi.fn().mockResolvedValue(undefined) } registerSshFilesystemProvider('ssh-1', fsProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await expect(runtime.readRepoIssueCommand('id:repo-1')).resolves.toMatchObject({ localContent: null, sharedContent: 'claude -p "Fix #{{issue}}"', effectiveContent: 'claude -p "Fix #{{issue}}"', localFilePath: '/remote/repo/.orca/issue-command', source: 'shared' }) await expect(runtime.writeRepoIssueCommand('id:repo-1', ' ')).resolves.toEqual({ ok: true }) } finally { unregisterSshFilesystemProvider('ssh-1') } expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/orca.yaml') expect(fsProvider.deletePath).toHaveBeenCalledWith('/remote/repo/.orca/issue-command', false) expect(fsProvider.writeFile).not.toHaveBeenCalledWith( '/remote/repo/.orca/issue-command', expect.anything() ) }) it('allows host integration slug helpers for SSH repos through provider-aware GitHub clients', async () => { const prRepo = { owner: 'acme', repo: 'orca', host: 'github.acme.test' } getIssueMock.mockResolvedValueOnce({ number: 12, title: 'Remote issue' }) listGitHubIssuesMock.mockResolvedValueOnce({ items: [{ number: 7, title: 'Remote issue list item' }] }) const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ] } const runtime = new OrcaRuntimeService(remoteStore as never) await expect(runtime.getRepoSlug('id:repo-1')).resolves.toBeNull() await expect(runtime.getRepoIssue('id:repo-1', 12)).resolves.toEqual({ number: 12, title: 'Remote issue' }) await expect(runtime.listRepoIssues('id:repo-1', 10)).resolves.toEqual([ { number: 7, title: 'Remote issue list item' } ]) await expect(runtime.requestRepoPRReviewers('id:repo-1', 7, ['alex'], prRepo)).resolves.toEqual( { ok: true } ) await expect(runtime.removeRepoPRReviewers('id:repo-1', 7, ['alex'], prRepo)).resolves.toEqual({ ok: true }) expect(getIssueMock).toHaveBeenCalledWith('/remote/repo', 12, 'ssh-1') expect(listGitHubIssuesMock).toHaveBeenCalledWith('/remote/repo', 10, undefined, 'ssh-1') expect(requestGitHubPRReviewersMock).toHaveBeenCalledWith( '/remote/repo', 7, ['alex'], 'ssh-1', prRepo ) expect(removeGitHubPRReviewersMock).toHaveBeenCalledWith( '/remote/repo', 7, ['alex'], 'ssh-1', prRepo ) }) it('routes runtime GitHub repo identity helpers through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) getRepoSlugMock.mockResolvedValueOnce({ owner: 'acme', repo: 'orca' }) getRepoUpstreamMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' }) await expect(runtime.getRepoSlug('id:repo-1')).resolves.toEqual({ owner: 'acme', repo: 'orca' }) await expect(runtime.getRepoUpstream('id:repo-1')).resolves.toEqual({ owner: 'stablyai', repo: 'orca' }) const runtimeOptions = { localGitExecOptions: { wslDistro: 'Ubuntu' } } expect(getRepoSlugMock).toHaveBeenCalledWith(TEST_REPO_PATH, null, runtimeOptions) expect(getRepoUpstreamMock).toHaveBeenCalledWith(TEST_REPO_PATH, null, runtimeOptions) }) it('routes runtime GitHub issue and work-item actions through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const localGitOptions = { wslDistro: 'Ubuntu' } const issueFields = { labels: ['bug'], assignees: ['octo'] } const issueUpdates = { body: 'Updated body' } listGitHubWorkItemsMock.mockResolvedValueOnce({ items: [] }) countGitHubWorkItemsMock.mockResolvedValueOnce(0) listGitHubIssuesMock.mockResolvedValueOnce({ items: [] }) getIssueMock.mockResolvedValueOnce(null) createGitHubIssueMock.mockResolvedValueOnce({ ok: true, number: 12, url: 'https://github.com/acme/orca/issues/12' }) updateGitHubIssueMock.mockResolvedValueOnce({ ok: true }) addGitHubIssueCommentMock.mockResolvedValueOnce({ ok: true }) listGitHubLabelsMock.mockResolvedValueOnce([]) listGitHubAssignableUsersMock.mockResolvedValueOnce([]) await runtime.listRepoWorkItems('id:repo-1', 7, 'is:open', 1, true) await runtime.countRepoWorkItems('id:repo-1', 'is:issue') await runtime.listRepoIssues('id:repo-1', 5) await runtime.getRepoIssue('id:repo-1', 12) await runtime.createRepoIssue('id:repo-1', 'Title', 'Body', issueFields) await runtime.updateRepoIssue('id:repo-1', 12, issueUpdates) await runtime.addRepoIssueComment('id:repo-1', 12, 'Comment') await runtime.listRepoLabels('id:repo-1') await runtime.listRepoAssignableUsers('id:repo-1') expect(listGitHubWorkItemsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 7, 'is:open', 1, undefined, null, true, localGitOptions ) expect(countGitHubWorkItemsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'is:issue', undefined, null, localGitOptions ) expect(listGitHubIssuesMock).toHaveBeenCalledWith( TEST_REPO_PATH, 5, undefined, null, localGitOptions ) expect(getIssueMock).toHaveBeenCalledWith(TEST_REPO_PATH, 12, null, localGitOptions) expect(createGitHubIssueMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'Title', 'Body', undefined, null, issueFields, localGitOptions ) expect(updateGitHubIssueMock).toHaveBeenCalledWith( TEST_REPO_PATH, 12, issueUpdates, null, localGitOptions ) expect(addGitHubIssueCommentMock).toHaveBeenCalledWith( TEST_REPO_PATH, 12, 'Comment', null, null, localGitOptions ) expect(listGitHubLabelsMock).toHaveBeenCalledWith( TEST_REPO_PATH, undefined, null, localGitOptions ) expect(listGitHubAssignableUsersMock).toHaveBeenCalledWith( TEST_REPO_PATH, undefined, null, localGitOptions ) }) it('routes runtime GitHub PR details and actions through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const localGitOptions = { wslDistro: 'Ubuntu' } const prRepo = { owner: 'acme', repo: 'orca', host: 'github.acme.test' } await runtime.getRepoPRForBranch('id:repo-1', 'feature/wsl', 42, 43) await runtime.getRepoWorkItem('id:repo-1', 42, 'pr') await runtime.getRepoWorkItemByOwnerRepo('id:repo-1', prRepo, 42, 'pr') await runtime.getRepoWorkItemDetails('id:repo-1', 42, 'pr') await runtime.getRepoPRChecks('id:repo-1', 42, 'head-sha', prRepo, { noCache: true }) await runtime.rerunRepoPRChecks('id:repo-1', 42, { headSha: 'head-sha', failedOnly: true, prRepo }) await runtime.getRepoPRCheckDetails('id:repo-1', { checkRunId: 9, workflowRunId: 8, checkName: 'lint', url: 'https://example.com/check', prRepo }) await runtime.getRepoPRComments('id:repo-1', 42, prRepo, { noCache: true }) await runtime.getRepoPRFileContents('id:repo-1', { prNumber: 42, prRepo, path: 'src/app.ts', status: 'modified', headSha: 'head-sha', baseSha: 'base-sha' }) await runtime.resolveRepoReviewThread('id:repo-1', 'thread-1', true, prRepo) await runtime.setRepoPRFileViewed('id:repo-1', { prRepo, pullRequestId: 'PR_kw', path: 'src/app.ts', viewed: true }) await runtime.updateRepoPRTitle('id:repo-1', 42, 'New title', prRepo) await runtime.updateRepoPRDetails('id:repo-1', 42, { body: 'New body' }, prRepo) await runtime.mergeRepoPR('id:repo-1', 42, 'squash', prRepo) await runtime.setRepoPRAutoMerge('id:repo-1', 42, true, 'squash', prRepo) await runtime.updateRepoPRState('id:repo-1', 42, { state: 'closed' }, prRepo) await runtime.requestRepoPRReviewers('id:repo-1', 42, ['octo'], prRepo) await runtime.removeRepoPRReviewers('id:repo-1', 42, ['octo'], prRepo) await runtime.addRepoPRReviewComment('id:repo-1', { prNumber: 42, prRepo, body: 'Inline', commitId: 'head-sha', path: 'src/app.ts', line: 10 }) await runtime.addRepoPRReviewCommentReply('id:repo-1', { prNumber: 42, commentId: 11, body: 'Reply', threadId: 'thread-1', path: 'src/app.ts', line: 10, prRepo }) expect(getPRForBranchOutcomeMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'feature/wsl', 42, null, null, { localGitExecOptions: localGitOptions } ) expect(getGitHubWorkItemMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, 'pr', null, localGitOptions ) expect(getGitHubWorkItemByOwnerRepoMock).toHaveBeenCalledWith( TEST_REPO_PATH, prRepo, 42, 'pr', null, localGitOptions ) expect(getGitHubWorkItemDetailsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, 'pr', null, localGitOptions ) expect(getGitHubPRChecksMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, 'head-sha', prRepo, { noCache: true }, null, localGitOptions ) expect(rerunGitHubPRChecksMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, { headSha: 'head-sha', failedOnly: true, prRepo }, null, localGitOptions ) expect(getGitHubPRCheckDetailsMock).toHaveBeenCalledWith( TEST_REPO_PATH, { checkRunId: 9, workflowRunId: 8, checkName: 'lint', url: 'https://example.com/check', prRepo }, null, localGitOptions ) expect(getGitHubPRCommentsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, { noCache: true, prRepo }, null, localGitOptions ) expect(getGitHubPRFileContentsMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: TEST_REPO_PATH, localGitOptions, prRepo }) ) expect(resolveGitHubReviewThreadMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'thread-1', true, null, prRepo, localGitOptions ) expect(setGitHubPRFileViewedMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: TEST_REPO_PATH, localGitOptions, prRepo }) ) expect(updateGitHubPRTitleMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, 'New title', null, prRepo, localGitOptions ) expect(updateGitHubPRDetailsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, { body: 'New body' }, null, prRepo, localGitOptions ) expect(mergeGitHubPRMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, 'squash', null, prRepo, localGitOptions ) expect(setGitHubPRAutoMergeMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, true, 'squash', null, prRepo, localGitOptions ) expect(updateGitHubPRStateMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, { state: 'closed' }, null, prRepo, localGitOptions ) expect(requestGitHubPRReviewersMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, ['octo'], null, prRepo, localGitOptions ) expect(removeGitHubPRReviewersMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, ['octo'], null, prRepo, localGitOptions ) expect(addGitHubPRReviewCommentMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: TEST_REPO_PATH, localGitOptions, prRepo, body: 'Inline' }) ) expect(addGitHubPRReviewCommentReplyMock).toHaveBeenCalledWith( TEST_REPO_PATH, 42, 11, 'Reply', 'thread-1', 'src/app.ts', 10, null, prRepo, localGitOptions ) }) it('rejects hosted review worktree selectors outside the selected repo', async () => { vi.mocked(listWorktrees).mockImplementation(async (repoPath: string) => { if (repoPath === '/tmp/repo-b') { return [ { path: '/tmp/worktree-b', head: 'def', branch: 'feature/bar', isBare: false, isMainWorktree: false } ] } return MOCK_GIT_WORKTREES }) const repos = [ { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1 }, { id: 'repo-2', path: '/tmp/repo-b', displayName: 'repo-b', badgeColor: 'green', addedAt: 2 } ] const multiRepoStore = { ...store, getRepos: () => repos, getRepo: (id: string) => repos.find((repo) => repo.id === id) } const runtime = new OrcaRuntimeService(multiRepoStore as never) await expect( runtime.getHostedReviewCreationEligibility({ repoSelector: 'id:repo-1', worktreeSelector: 'id:repo-2::/tmp/worktree-b', branch: 'feature/bar', base: 'main', hasUncommittedChanges: false, hasUpstream: true, ahead: 1, behind: 0 }) ).rejects.toThrow('Access denied: worktree does not belong to repository') await expect( runtime.createHostedReview({ repoSelector: 'id:repo-1', worktreeSelector: 'id:repo-2::/tmp/worktree-b', provider: 'github', base: 'main', head: 'feature/bar', title: 'Create PR', body: '', draft: false }) ).rejects.toThrow('Access denied: worktree does not belong to repository') expect(getHostedReviewCreationEligibilityMock).not.toHaveBeenCalled() expect(createHostedReviewMock).not.toHaveBeenCalled() }) it('passes SSH connection context through hosted review creation flows', async () => { const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const remoteStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined) } const runtime = new OrcaRuntimeService(remoteStore as never) await runtime.getHostedReviewCreationEligibility({ repoSelector: `id:${TEST_REPO_ID}`, branch: 'feature/ssh', base: 'main', hasUncommittedChanges: false, hasUpstream: true, ahead: 0, behind: 0 }) await runtime.createHostedReview({ repoSelector: `id:${TEST_REPO_ID}`, provider: 'github', base: 'main', head: 'feature/ssh', title: 'Feature SSH', body: '', draft: false }) expect(getHostedReviewCreationEligibilityMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: '/remote/repo', connectionId: 'ssh-1', branch: 'feature/ssh' }) ) expect(createHostedReviewMock).toHaveBeenCalledWith( '/remote/repo', expect.objectContaining({ provider: 'github', head: 'feature/ssh', title: 'Feature SSH' }), 'ssh-1' ) }) it('routes local WSL project hosted review flows through runtime git options', async () => { setPlatform('win32') const wslStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(wslStore as never) getHostedReviewForBranchMock.mockResolvedValueOnce({ provider: 'github', number: 76, title: 'Feature WSL', state: 'open', url: 'https://github.com/acme/orca/pull/76', status: 'success', updatedAt: '2026-06-16T00:00:00.000Z', mergeable: 'MERGEABLE' }) createHostedReviewMock.mockResolvedValueOnce({ ok: true, number: 77, url: 'https://github.com/acme/orca/pull/77' }) await runtime.getHostedReviewForBranch({ repoSelector: `id:${TEST_REPO_ID}`, branch: 'feature/wsl', linkedGitHubPR: 76 }) await runtime.getHostedReviewCreationEligibility({ repoSelector: `id:${TEST_REPO_ID}`, branch: 'feature/wsl', base: 'main', hasUncommittedChanges: false, hasUpstream: true, ahead: 0, behind: 0 }) await runtime.createHostedReview({ repoSelector: `id:${TEST_REPO_ID}`, provider: 'github', base: 'main', head: 'feature/wsl', title: 'Feature WSL', body: '', draft: false }) expect(getHostedReviewCreationEligibilityMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: TEST_REPO_PATH, connectionId: null, branch: 'feature/wsl', localGitExecOptions: { wslDistro: 'Ubuntu' } }) ) expect(getHostedReviewForBranchMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: TEST_REPO_PATH, connectionId: null, branch: 'feature/wsl', linkedGitHubPR: 76, localGitExecOptions: { wslDistro: 'Ubuntu' } }) ) expect(createHostedReviewMock).toHaveBeenCalledWith( TEST_REPO_PATH, expect.objectContaining({ provider: 'github', head: 'feature/wsl', title: 'Feature WSL' }), null, { localGitExecOptions: { wslDistro: 'Ubuntu' } } ) }) it('treats SSH worktree drift as unknown without local git probes', async () => { vi.mocked(listWorktrees).mockClear() vi.mocked(getBaseRefDefault).mockClear() const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getWorktreeMeta: () => null } const gitProvider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'abc', branch: 'feature/foo', isBare: false, isMainWorktree: true } ]) } registerSshGitProvider('ssh-1', gitProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await expect(runtime.probeWorktreeDrift('path:/remote/repo')).resolves.toBeNull() } finally { unregisterSshGitProvider('ssh-1') } expect(gitProvider.listWorktrees).toHaveBeenCalledWith('/remote/repo') expect(getBaseRefDefault).not.toHaveBeenCalled() expect(listWorktrees).not.toHaveBeenCalled() }) it('routes local WSL project worktree drift probes through runtime git options', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const wslGitOptions = { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' } const asyncGitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: `${TEST_REPO_PATH}/.git\n`, stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }) const syncGitSpy = vi .spyOn(gitRunner, 'gitExecFileSync') .mockImplementation((args: string[]) => { if (args[0] === 'rev-list') { return '1\t2\n' } if (args[0] === 'log') { return 'base commit 2\nbase commit 1\n' } throw new Error(`unexpected sync git call: ${args.join(' ')}`) }) try { const result = await runtime.probeWorktreeDrift(`id:${TEST_WORKTREE_ID}`) expect(result).toEqual({ base: 'origin/main', behind: 2, recentSubjects: ['base commit 2', 'base commit 1'] }) expect(asyncGitSpy).toHaveBeenCalledWith( ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { ...wslGitOptions, timeout: 15_000 } ) expect(asyncGitSpy).toHaveBeenCalledWith(['remote'], wslGitOptions) expect(asyncGitSpy).toHaveBeenCalledWith( ['rev-parse', '--path-format=absolute', '--git-common-dir'], wslGitOptions ) expect(asyncGitSpy).toHaveBeenCalledWith(['fetch', 'origin'], { ...wslGitOptions, timeout: 60_000 }) expect(syncGitSpy).toHaveBeenCalledWith( ['rev-list', '--left-right', '--count', 'HEAD...origin/main'], { cwd: TEST_WORKTREE_PATH, wslDistro: 'Ubuntu' } ) expect(syncGitSpy).toHaveBeenCalledWith( ['log', '--format=%s', '-n', '5', 'HEAD..origin/main'], { cwd: TEST_WORKTREE_PATH, wslDistro: 'Ubuntu' } ) } finally { asyncGitSpy.mockRestore() syncGitSpy.mockRestore() } }) it('deduplicates runtime repo paths with Windows/UNC comparison semantics', async () => { const added: Record[] = [] const uncStore = { ...store, getRepos: () => [ { id: 'repo-unc', path: '//Server/Share/Repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, kind: 'folder' }, ...added ], addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => [...uncStore.getRepos()].find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(uncStore as never) const repo = await runtime.addRepo('//server/share/repo', 'folder') expect(repo).toMatchObject({ id: 'repo-unc', path: '//Server/Share/Repo' }) expect(added).toHaveLength(0) }) it('browses runtime server directories before projects are added', async () => { const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-browse-')) try { await mkdir(join(tempRoot, 'zeta')) await mkdir(join(tempRoot, 'alpha')) await writeFile(join(tempRoot, 'readme.md'), '# Readme\n') const runtime = new OrcaRuntimeService(store) const result = await runtime.browseServerDir(tempRoot) expect(result.resolvedPath).toBe(tempRoot) expect(result.entries).toEqual([ { name: 'alpha', isDirectory: true, isSymlink: false }, { name: 'zeta', isDirectory: true, isSymlink: false }, { name: 'readme.md', isDirectory: false, isSymlink: false } ]) } finally { await rm(tempRoot, { recursive: true, force: true }) } }) it('defaults runtime addRepo badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => { const added: Record[] = [] const colorStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => added.find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(colorStore as never) const repo = await runtime.addRepo('/tmp/runtime-add-default', 'folder') expect(repo.badgeColor).toBe(DEFAULT_REPO_BADGE_COLOR) expect(added).toEqual([expect.objectContaining({ badgeColor: DEFAULT_REPO_BADGE_COLOR })]) }) it('prepares the runtime worktree root when adding a repo', async () => { const added: Record[] = [] const runtimeStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => added.find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(runtimeStore as never) const repo = await runtime.addRepo('/tmp/runtime-add-root-prep', 'folder') expect(prepareLocalWorktreeRootForRepoMock).toHaveBeenCalledWith(runtimeStore, repo) }) it('sets up an existing folder on a fresh runtime after importing the repo project', async () => { const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-project-setup-')) const repos: Record[] = [] getRepoUpstreamMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' }) const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never, updateRepo: (id: string, updates: Record) => { const index = repos.findIndex((repo) => repo.id === id) if (index === -1) { return null } repos[index] = { ...repos[index], ...updates } return repos[index] as never }, getProjects: () => repos .map((repo) => { const upstream = repo.upstream as { owner: string; repo: string } | undefined if (!upstream) { return null } return { id: `github:${upstream.owner}/${upstream.repo}`, displayName: repo.displayName, badgeColor: repo.badgeColor, providerIdentity: { provider: 'github', owner: upstream.owner, repo: upstream.repo }, sourceRepoIds: [repo.id], createdAt: repo.addedAt, updatedAt: repo.addedAt } }) .filter(Boolean) as never, getProjectHostSetups: () => repos.map((repo) => { const upstream = repo.upstream as { owner: string; repo: string } | undefined return { id: repo.id, projectId: upstream ? `github:${upstream.owner}/${upstream.repo}` : repo.id, hostId: 'local', repoId: repo.id, path: repo.path, displayName: repo.displayName, kind: repo.kind, setupState: 'ready', setupMethod: repo.projectHostSetupMethod ?? 'legacy-repo', createdAt: repo.addedAt, updatedAt: repo.addedAt } }) as never } const runtime = new OrcaRuntimeService(runtimeStore as never) try { execFileSync('git', ['init'], { cwd: tempRoot, stdio: 'ignore' }) const result = await runtime.setupProjectExistingFolder({ projectId: 'github:stablyai/orca', hostId: 'runtime:env-1', path: tempRoot, kind: 'git', setupMethod: 'imported-existing-folder' }) expect(result.project.id).toBe('github:stablyai/orca') expect(result.repo.path).toBe(tempRoot) expect(result.setup).toMatchObject({ projectId: 'github:stablyai/orca', path: tempRoot, setupMethod: 'imported-existing-folder' }) } finally { await rm(tempRoot, { recursive: true, force: true }) } }) it('keeps existing-folder imports split by runtime host on the same normalized path', async () => { const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-project-host-')) const repos: Record[] = [] getRepoUpstreamMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' }) const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never, updateRepo: (id: string, updates: Record) => { const index = repos.findIndex((repo) => repo.id === id) if (index === -1) { return null } repos[index] = { ...repos[index], ...updates } return repos[index] as never }, getProjects: () => projectHostSetupProjectionFromRepos(repos as never).projects as never, getProjectHostSetups: () => projectHostSetupProjectionFromRepos(repos as never).setups as never } const runtime = new OrcaRuntimeService(runtimeStore as never) try { execFileSync('git', ['init'], { cwd: tempRoot, stdio: 'ignore' }) const first = await runtime.setupProjectExistingFolder({ projectId: 'github:stablyai/orca', hostId: 'runtime:env-1', path: tempRoot, kind: 'git', setupMethod: 'imported-existing-folder' }) const second = await runtime.setupProjectExistingFolder({ projectId: 'github:stablyai/orca', hostId: 'runtime:env-2', path: tempRoot, kind: 'git', setupMethod: 'imported-existing-folder' }) expect(repos).toHaveLength(2) expect(repos).toEqual([ expect.objectContaining({ path: tempRoot, executionHostId: 'runtime:env-1' }), expect.objectContaining({ path: tempRoot, executionHostId: 'runtime:env-2' }) ]) expect(first.repo).toMatchObject({ path: tempRoot, executionHostId: 'runtime:env-1' }) expect(first.setup).toMatchObject({ repoId: first.repo.id, hostId: 'runtime:env-1' }) expect(second.repo).toMatchObject({ path: tempRoot, executionHostId: 'runtime:env-2' }) expect(second.setup).toMatchObject({ repoId: second.repo.id, hostId: 'runtime:env-2' }) expect(first.repo.id).not.toBe(second.repo.id) expect(first.setup.repoId).not.toBe(second.setup.repoId) } finally { await rm(tempRoot, { recursive: true, force: true }) } }) it('keeps path-only runtime addRepo reuse working when a host-qualified repo already exists', async () => { const repos: Record[] = [ { id: 'repo-runtime-1', path: '/tmp/runtime-shared', displayName: 'repo', badgeColor: 'blue', addedAt: 1, kind: 'folder', executionHostId: 'runtime:env-1' } ] const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(runtimeStore as never) const repo = await runtime.addRepo('/tmp/runtime-shared', 'folder') expect(repo).toMatchObject({ id: 'repo-runtime-1', path: '/tmp/runtime-shared', executionHostId: 'runtime:env-1' }) expect(repos).toHaveLength(1) }) it('does not hijack a legacy SSH repo at the same path into a runtime host', async () => { // A legacy SSH repo resolves to `ssh:` even with null executionHostId, so a same-path runtime import creates a new repo instead of adopting it. const repos: Record[] = [ { id: 'repo-ssh-1', path: '/workspace', displayName: 'workspace', badgeColor: 'blue', addedAt: 1, kind: 'folder', connectionId: 'ssh-target-1' } ] const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never, updateRepo: (id: string, updates: Record) => { const index = repos.findIndex((repo) => repo.id === id) if (index === -1) { return null } repos[index] = { ...repos[index], ...updates } return repos[index] as never } } const runtime = new OrcaRuntimeService(runtimeStore as never) const repo = await runtime.addRepo('/workspace', 'folder', 'runtime:env-1') expect(repos).toHaveLength(2) expect(repo.id).not.toBe('repo-ssh-1') expect(repo).toMatchObject({ path: '/workspace', executionHostId: 'runtime:env-1' }) // The legacy SSH repo must be untouched (no executionHostId stamped onto it). expect(repos[0]).toMatchObject({ id: 'repo-ssh-1', connectionId: 'ssh-target-1' }) expect(repos[0]).not.toHaveProperty('executionHostId') }) it('only a runtime host adopts an unstamped repo; local/ssh imports never stamp it', async () => { // Local and legacy runtime repos both have null executionHostId/connectionId, so only a runtime host may backfill; local/ssh imports leave it untouched. for (const importHostId of ['local', 'ssh:ssh-target-9'] as const) { const repos: Record[] = [ { id: 'repo-local-1', path: '/workspace', displayName: 'workspace', badgeColor: 'blue', addedAt: 1, kind: 'folder' } ] const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never, updateRepo: (id: string, updates: Record) => { const index = repos.findIndex((repo) => repo.id === id) if (index === -1) { return null } repos[index] = { ...repos[index], ...updates } return repos[index] as never } } const runtime = new OrcaRuntimeService(runtimeStore as never) const repo = await runtime.addRepo('/workspace', 'folder', importHostId) // The matched repo is returned unchanged — no new repo, no executionHostId stamped. expect(repos).toHaveLength(1) expect(repo.id).toBe('repo-local-1') expect(repos[0]).not.toHaveProperty('executionHostId') } }) it('keeps project clone setup on the cloned host-qualified repo', async () => { const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-')) const clonePath = join(destination, 'orca') const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const repos: Record[] = [] getRepoUpstreamMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' }) const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never, updateRepo: (id: string, updates: Record) => { const index = repos.findIndex((repo) => repo.id === id) if (index === -1) { return null } repos[index] = { ...repos[index], ...updates } return repos[index] as never }, getProjects: () => projectHostSetupProjectionFromRepos(repos as never).projects as never, getProjectHostSetups: () => projectHostSetupProjectionFromRepos(repos as never).setups as never } spawnSpy.mockImplementation(() => { const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() queueMicrotask(() => { mkdirSync(clonePath, { recursive: true }) execFileSync('git', ['init'], { cwd: clonePath, stdio: 'ignore' }) proc.emit('close', 0, null) }) return proc as never }) const runtime = new OrcaRuntimeService(runtimeStore as never) try { const result = await runtime.setupProjectClone({ projectId: 'github:stablyai/orca', hostId: 'runtime:env-1', url: 'https://example.com/orca.git', destination }) expect(repos).toHaveLength(1) expect(result.repo).toMatchObject({ path: clonePath, executionHostId: 'runtime:env-1', projectHostSetupMethod: 'cloned' }) expect(result.setup).toMatchObject({ repoId: result.repo.id, hostId: 'runtime:env-1', setupMethod: 'cloned' }) } finally { spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('adopts public clone repos into host-qualified project setup', async () => { const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-')) const clonePath = join(destination, 'orca') const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const repos: Record[] = [] getRepoUpstreamMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' }) const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never, updateRepo: (id: string, updates: Record) => { const index = repos.findIndex((repo) => repo.id === id) if (index === -1) { return null } repos[index] = { ...repos[index], ...updates } return repos[index] as never }, getProjects: () => projectHostSetupProjectionFromRepos(repos as never).projects as never, getProjectHostSetups: () => projectHostSetupProjectionFromRepos(repos as never).setups as never } spawnSpy.mockImplementation(() => { const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() queueMicrotask(() => { mkdirSync(clonePath, { recursive: true }) execFileSync('git', ['init'], { cwd: clonePath, stdio: 'ignore' }) proc.emit('close', 0, null) }) return proc as never }) const runtime = new OrcaRuntimeService(runtimeStore as never) try { const cloned = await runtime.cloneRepo('https://example.com/orca.git', destination) expect(cloned).toMatchObject({ path: clonePath }) expect(cloned).not.toHaveProperty('executionHostId') const result = await runtime.setupProjectExistingFolder({ projectId: 'github:stablyai/orca', hostId: 'runtime:env-1', path: clonePath, kind: 'git', setupMethod: 'cloned' }) expect(repos).toHaveLength(1) expect(result.repo).toMatchObject({ id: cloned.id, path: clonePath, executionHostId: 'runtime:env-1', projectHostSetupMethod: 'cloned' }) expect(result.setup).toMatchObject({ repoId: cloned.id, hostId: 'runtime:env-1', setupMethod: 'cloned' }) } finally { spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('keeps project clone repos split by runtime host on the same clone path', async () => { const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-')) const clonePath = join(destination, 'orca') const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const repos: Record[] = [ { id: 'repo-host-a', path: clonePath, displayName: 'orca', badgeColor: 'blue', addedAt: 1, kind: 'git', executionHostId: 'runtime:env-1' } ] getRepoUpstreamMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' }) const runtimeStore = { ...store, getRepos: () => [...repos] as never, addRepo: (repo: Record) => { repos.push(repo) }, getRepo: (id: string) => repos.find((repo) => repo.id === id) as never, updateRepo: (id: string, updates: Record) => { const index = repos.findIndex((repo) => repo.id === id) if (index === -1) { return null } repos[index] = { ...repos[index], ...updates } return repos[index] as never }, getProjects: () => projectHostSetupProjectionFromRepos(repos as never).projects as never, getProjectHostSetups: () => projectHostSetupProjectionFromRepos(repos as never).setups as never } spawnSpy.mockImplementation(() => { const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() queueMicrotask(() => { mkdirSync(clonePath, { recursive: true }) execFileSync('git', ['init'], { cwd: clonePath, stdio: 'ignore' }) proc.emit('close', 0, null) }) return proc as never }) const runtime = new OrcaRuntimeService(runtimeStore as never) try { const result = await runtime.setupProjectClone({ projectId: 'github:stablyai/orca', hostId: 'runtime:env-2', url: 'https://example.com/orca.git', destination }) expect(repos).toHaveLength(2) expect(repos[0]).toMatchObject({ id: 'repo-host-a', executionHostId: 'runtime:env-1' }) expect(result.repo).toMatchObject({ path: clonePath, executionHostId: 'runtime:env-2' }) expect(result.repo.id).not.toBe('repo-host-a') expect(result.setup).toMatchObject({ repoId: result.repo.id, hostId: 'runtime:env-2', setupMethod: 'cloned' }) } finally { spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('defaults runtime createRepo badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => { const added: Record[] = [] const colorStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => added.find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(colorStore as never) const parentDir = await mkdtemp('/tmp/orca-runtime-create-') try { const result = await runtime.createRepo(parentDir, 'runtime-create-default', 'folder') if ('error' in result) { throw new Error(result.error) } expect(result).toHaveProperty('repo.badgeColor', DEFAULT_REPO_BADGE_COLOR) expect(added).toEqual([expect.objectContaining({ badgeColor: DEFAULT_REPO_BADGE_COLOR })]) } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('creates a missing runtime parent before creating the project directory', async () => { const added: Record[] = [] const createStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => added.find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(createStore as never) const tempRoot = await mkdtemp('/tmp/orca-runtime-create-parent-') const parentDir = join(tempRoot, 'orca', 'projects') try { const result = await runtime.createRepo(parentDir, 'first-project', 'folder') if ('error' in result) { throw new Error(result.error) } expect((await lstat(parentDir)).isDirectory()).toBe(true) expect((await lstat(join(parentDir, 'first-project'))).isDirectory()).toBe(true) expect(result).toHaveProperty('repo.path', join(parentDir, 'first-project')) } finally { await rm(tempRoot, { recursive: true, force: true }) } }) it('prepares the runtime worktree root when creating a repo', async () => { const added: Record[] = [] const runtimeStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => added.find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(runtimeStore as never) const parentDir = await mkdtemp('/tmp/orca-runtime-create-root-prep-') try { const result = await runtime.createRepo(parentDir, 'runtime-create-root-prep', 'folder') if ('error' in result) { throw new Error(result.error) } expect(prepareLocalWorktreeRootForRepoMock).toHaveBeenCalledWith(runtimeStore, result.repo) } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('preserves existing badgeColor on runtime createRepo dedupe', async () => { const existing = { id: 'runtime-existing-create', path: '/tmp/runtime-existing-create', displayName: 'runtime-existing-create', badgeColor: '#14b8a6', addedAt: 1, kind: 'folder' as const } const colorStore = { ...store, getRepos: () => [existing] } const runtime = new OrcaRuntimeService(colorStore as never) const result = await runtime.createRepo('/tmp', 'runtime-existing-create', 'folder') expect(result).toEqual({ repo: existing }) expect(result).toHaveProperty('repo.badgeColor', '#14b8a6') }) it('defaults runtime cloneRepo badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const added: Record[] = [] const colorStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => added.find((repo) => repo.id === id) as never } spawnSpy.mockImplementation(() => { const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() queueMicrotask(() => proc.emit('close', 0, null)) return proc as never }) const runtime = new OrcaRuntimeService(colorStore as never) try { const repo = await runtime.cloneRepo('https://example.com/repo-badge-color.git', '/tmp') expect(repo.badgeColor).toBe(DEFAULT_REPO_BADGE_COLOR) expect(added).toEqual([ expect.objectContaining({ badgeColor: DEFAULT_REPO_BADGE_COLOR, externalWorktreeVisibility: 'hide', externalWorktreeVisibilityLegacy: false }) ]) expect(repo.externalWorktreeVisibility).toBe('hide') expect(prepareLocalWorktreeRootForRepoMock).toHaveBeenCalledWith(colorStore, repo) } finally { spawnSpy.mockRestore() } }) it('drops a same-path negative submodule cache before runtime cloneRepo', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-reclone-')) const clonePath = join(destination, 'reclone') const added: Record[] = [] const cloneStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => added.push(repo), getRepo: (id: string) => added.find((repo) => repo.id === id) as never } spawnSpy.mockImplementation(() => { const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() queueMicrotask(() => { void mkdir(clonePath, { recursive: true }) .then(() => writeFile(join(clonePath, '.gitmodules'), '[submodule "lib"]\n\tpath = vendor/lib\n') ) .then(() => proc.emit('close', 0, null)) }) return proc as never }) const runtime = new OrcaRuntimeService(cloneStore as never) try { clearSubmodulePathsCacheForTests() await expect(listSubmodulePaths(clonePath)).resolves.toEqual([]) await runtime.cloneRepo('https://example.com/reclone.git', destination) await expect(listSubmodulePaths(clonePath)).resolves.toEqual(['vendor/lib']) } finally { clearSubmodulePathsCacheForTests() spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('preserves existing badgeColor on runtime cloneRepo folder->git dedupe upgrade', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') spawnSpy.mockImplementation(() => { const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() queueMicrotask(() => proc.emit('close', 0, null)) return proc as never }) const existing = { id: 'runtime-folder-upgrade', path: '/tmp/repo-badge-color', displayName: 'repo-badge-color', badgeColor: '#ec4899', addedAt: 1, kind: 'folder' as const } const updates: { id: string; updates: Record }[] = [] const upgraded = { ...existing, kind: 'git' as const } const colorStore = { ...store, getRepos: () => [existing], updateRepo: (id: string, repoUpdates: Record) => { updates.push({ id, updates: repoUpdates }) return upgraded as never } } const runtime = new OrcaRuntimeService(colorStore as never) try { const repo = await runtime.cloneRepo('https://example.com/repo-badge-color.git', '/tmp') expect(updates).toEqual([{ id: existing.id, updates: { kind: 'git' } }]) expect(repo).toEqual(upgraded) expect(repo.badgeColor).toBe('#ec4899') expect(prepareLocalWorktreeRootForRepoMock).toHaveBeenCalledWith(colorStore, upgraded) expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled() } finally { spawnSpy.mockRestore() } }) it('prepares the runtime worktree root when worktree base path changes', async () => { const repo = { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1, kind: 'git' as const } const updated = { ...repo, worktreeBasePath: '../worktrees' } const runtimeStore = { ...store, getRepos: () => [repo], getRepo: (id: string) => (id === repo.id ? repo : undefined) as never, updateRepo: vi.fn(() => updated as never) } const runtime = new OrcaRuntimeService(runtimeStore as never) await expect(runtime.updateRepo(repo.id, { worktreeBasePath: '../worktrees' })).resolves.toBe( updated ) expect(prepareLocalWorktreeRootForRepoMock).toHaveBeenCalledWith(runtimeStore, updated) expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled() }) it('prepares the runtime worktree root when repo-backed project host setup base path changes', () => { const repo = { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1, kind: 'git' as const, worktreeBasePath: '../worktrees' } const result = { project: { id: 'project-1', displayName: 'Repo' }, setup: { id: 'setup-1', projectId: 'project-1', repoId: repo.id, hostId: 'local' }, repo } const runtimeStore = { ...store, updateProjectHostSetup: vi.fn(() => result as never) } const runtime = new OrcaRuntimeService(runtimeStore as never) expect( runtime.updateProjectHostSetup({ setupId: 'setup-1', updates: { worktreeBasePath: '../worktrees' } }) ).toBe(result) expect(prepareLocalWorktreeRootForRepoMock).toHaveBeenCalledWith(runtimeStore, repo) expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled() }) it('rejects runtime cloneRepo dot-segment URLs before spawning git', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const runtime = createRuntime() const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-')) const destination = join(tempRoot, 'destination') try { await expect(runtime.cloneRepo('file:///tmp/source/.', destination)).rejects.toThrow( 'Invalid repository name derived from URL' ) expect(spawnSpy).not.toHaveBeenCalled() } finally { spawnSpy.mockRestore() await rm(tempRoot, { recursive: true, force: true }) } }) it('rejects runtime cloneRepo parent-segment URLs before spawning git', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const runtime = createRuntime() const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-')) const destination = join(tempRoot, 'destination') try { await expect(runtime.cloneRepo('file:///tmp/source/..', destination)).rejects.toThrow( 'Invalid repository name derived from URL' ) expect(spawnSpy).not.toHaveBeenCalled() } finally { spawnSpy.mockRestore() await rm(tempRoot, { recursive: true, force: true }) } }) it('removes an owned runtime clone target when git exits unsuccessfully', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() spawnSpy.mockReturnValue(proc as never) const runtime = createRuntime() const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-')) const clonePath = join(destination, 'repo-badge-color') try { const clonePromise = runtime.cloneRepo( 'https://example.com/repo-badge-color.git', destination ) await vi.waitFor(() => expect(spawnSpy).toHaveBeenCalledTimes(1)) await writeFile(join(clonePath, 'partial.txt'), 'git wrote this before failing') proc.stderr.emit('data', Buffer.from('fatal: repository not found\n')) proc.emit('close', 128, null) await expect(clonePromise).rejects.toThrow('Clone failed: fatal: repository not found') await expect(lstat(clonePath)).rejects.toThrow() } finally { spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('preserves an existing runtime clone target when git exits unsuccessfully', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() spawnSpy.mockReturnValue(proc as never) const runtime = createRuntime() const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-')) const clonePath = join(destination, 'repo-badge-color') try { await mkdir(clonePath) await writeFile(join(clonePath, 'user-file.txt'), 'keep me') const clonePromise = runtime.cloneRepo( 'https://example.com/repo-badge-color.git', destination ) await vi.waitFor(() => expect(spawnSpy).toHaveBeenCalledTimes(1)) proc.emit('close', 128, null) await expect(clonePromise).rejects.toThrow('Clone failed') await expect(lstat(join(clonePath, 'user-file.txt'))).resolves.toBeTruthy() } finally { spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('skips runtime clone failure cleanup when the owned target is replaced', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } proc.stderr = new EventEmitter() spawnSpy.mockReturnValue(proc as never) const runtime = createRuntime() const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-')) const clonePath = join(destination, 'repo-badge-color') const replacementFile = join(clonePath, 'replacement.txt') try { const clonePromise = runtime.cloneRepo( 'https://example.com/repo-badge-color.git', destination ) await vi.waitFor(() => expect(spawnSpy).toHaveBeenCalledTimes(1)) await rm(clonePath, { recursive: true, force: true }) await mkdir(clonePath) await writeFile(replacementFile, 'new owner') proc.emit('close', 128, null) await expect(clonePromise).rejects.toThrow('Clone failed') await expect(lstat(replacementFile)).resolves.toBeTruthy() } finally { spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('serializes concurrent runtime clones for the same target', async () => { const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') const firstProc = new EventEmitter() as EventEmitter & { stderr: EventEmitter } firstProc.stderr = new EventEmitter() spawnSpy.mockReturnValueOnce(firstProc as never) const added: Record[] = [] const colorStore = { ...store, getRepos: () => [...added] as never, addRepo: (repo: Record) => { added.push(repo) }, getRepo: (id: string) => added.find((repo) => repo.id === id) as never } const runtime = new OrcaRuntimeService(colorStore as never) const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-')) try { const firstClonePromise = runtime.cloneRepo( 'https://example.com/repo-badge-color.git', destination ) const secondClonePromise = runtime.cloneRepo( 'https://example.com/repo-badge-color.git', destination ) await vi.waitFor(() => expect(spawnSpy).toHaveBeenCalledTimes(1)) await new Promise((resolve) => setImmediate(resolve)) expect(spawnSpy).toHaveBeenCalledTimes(1) firstProc.emit('close', 0, null) await expect(firstClonePromise).resolves.toMatchObject({ path: join(destination, 'repo-badge-color') }) await expect(secondClonePromise).resolves.toMatchObject({ path: join(destination, 'repo-badge-color') }) expect(spawnSpy).toHaveBeenCalledTimes(1) } finally { spawnSpy.mockRestore() await rm(destination, { recursive: true, force: true }) } }) it('associates controller PTYs with mixed-case Windows and UNC cwd paths', async () => { vi.mocked(listWorktrees).mockResolvedValue([ { path: 'C:\\Repo', head: 'abc', branch: 'feature/windows', isBare: false, isMainWorktree: true }, { path: '//Server/Share/Repo', head: 'def', branch: 'feature/unc', isBare: false, isMainWorktree: false } ]) const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'pty-windows', cwd: 'c:\\repo\\src', title: 'Windows shell' }, { id: 'pty-unc', cwd: '//server/share/repo/src', title: 'UNC shell' } ] }) runtime.attachWindow(1) runtime.markGraphReady(1) const terminals = await runtime.listTerminals() expect(terminals.terminals).toEqual( expect.arrayContaining([ expect.objectContaining({ worktreeId: `${TEST_REPO_ID}::C:\\Repo`, worktreePath: 'C:\\Repo' }), expect.objectContaining({ worktreeId: `${TEST_REPO_ID}:://Server/Share/Repo`, worktreePath: '//Server/Share/Repo' }) ]) ) }) it('uses OSC titles rather than controller process names for rendererless PTYs', async () => { const ptyId = `${TEST_REPO_ID}::/tmp/worktree-a@@pty-bg` const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [{ id: ptyId, cwd: '/tmp/worktree-a', title: 'shell' }] }) runtime.attachWindow(1) runtime.markGraphReady(1) expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: null }) runtime.onPtyData(ptyId, '\x1b]0;Codex\x07', 123) expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: 'Codex' }) expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: 'Codex' }) }) it('resolves tui-idle when a completion title is coalesced with the next working title', async () => { // Why: batching can coalesce "task done" + next working title into one chunk; a last-title reader misses the idle and hangs (#1083 class). const runtime = createRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100) const [terminal] = (await runtime.listTerminals()).terminals const wait = runtime.waitForTerminal(terminal.handle, { condition: 'tui-idle', timeoutMs: 1_000 }) runtime.onPtyData('pty-1', '\x1b]0;Codex done\x07\x1b]0;Codex working\x07', 101) await expect(wait).resolves.toMatchObject({ handle: terminal.handle, condition: 'tui-idle', status: 'running' }) }) it('ignores the bare cursor-agent native title so synthesized spinner state survives', async () => { const ptyId = `${TEST_REPO_ID}::/tmp/worktree-a@@pty-bg` const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [{ id: ptyId, cwd: '/tmp/worktree-a', title: 'shell' }] }) runtime.attachWindow(1) runtime.markGraphReady(1) runtime.onPtyData(ptyId, '\x1b]0;⠋ Cursor Agent\x07', 100) // cursor-agent re-emits its bare native title on internal redraws while still working; it must not stomp the synthesized working title. runtime.onPtyData(ptyId, '\x1b]0;Cursor Agent\x07', 101) expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: '⠋ Cursor Agent' }) }) it('clears a stale working title after 3s of title-less output', async () => { vi.useFakeTimers() try { const ptyId = `${TEST_REPO_ID}::/tmp/worktree-a@@pty-bg` const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [{ id: ptyId, cwd: '/tmp/worktree-a', title: 'shell' }] }) runtime.attachWindow(1) runtime.markGraphReady(1) runtime.onPtyData(ptyId, '\x1b]0;Codex working\x07', 100) runtime.onPtyData(ptyId, 'output without a title\r\n', 101) expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: 'Codex working' }) await vi.advanceTimersByTimeAsync(3_000) expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: 'Codex' }) } finally { vi.useRealTimers() } }) it('cancels the stale-title timer when the PTY exits', async () => { vi.useFakeTimers() try { const ptyId = `${TEST_REPO_ID}::/tmp/worktree-a@@pty-bg` const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [{ id: ptyId, cwd: '/tmp/worktree-a', title: 'shell' }] }) runtime.attachWindow(1) runtime.markGraphReady(1) runtime.onPtyData(ptyId, '\x1b]0;Codex working\x07', 100) runtime.onPtyData(ptyId, 'output without a title\r\n', 101) runtime.onPtyExit(ptyId, 0) await vi.advanceTimersByTimeAsync(4_000) // The dead session keeps its factual last title; the disposed tracker's stale-title rewrite must not fire into the retained record. expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: 'Codex working' }) } finally { vi.useRealTimers() } }) it('keeps stale-title timers isolated per PTY', async () => { vi.useFakeTimers() try { const ptyA = `${TEST_REPO_ID}::/tmp/worktree-a@@pty-a` const ptyB = `${TEST_REPO_ID}::/tmp/worktree-a@@pty-b` const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: ptyA, cwd: '/tmp/worktree-a', title: 'shell' }, { id: ptyB, cwd: '/tmp/worktree-a', title: 'shell' } ] }) runtime.attachWindow(1) runtime.markGraphReady(1) runtime.onPtyData(ptyA, '\x1b]0;Codex working\x07', 100) runtime.onPtyData(ptyB, '\x1b]0;Aider working\x07', 100) // Only A receives title-less output, so only A's stale timer arms. runtime.onPtyData(ptyA, 'output without a title\r\n', 101) await vi.advanceTimersByTimeAsync(3_000) const { terminals } = await runtime.listTerminals() expect(terminals.find((t) => t.tabId === `pty:${ptyA}`)).toMatchObject({ title: 'Codex' }) expect(terminals.find((t) => t.tabId === `pty:${ptyB}`)).toMatchObject({ title: 'Aider working' }) } finally { vi.useRealTimers() } }) // ─── pty:sideEffect channel (terminal-side-effect-authority.md, slice 2) ── describe('terminal side-effect fact channel', () => { function createSideEffectRuntime(): { runtime: OrcaRuntimeService batches: TerminalSideEffectBatch[] } { const batches: TerminalSideEffectBatch[] = [] const runtime = new OrcaRuntimeService(store, undefined, { onTerminalSideEffects: (batch) => batches.push(batch) }) return { runtime, batches } } it('defers desktop-only output scanners until a headless runtime is promoted', () => { const { runtime, batches } = createSideEffectRuntime() const trackerEntries = ( runtime as unknown as { ptyTitleTrackersByPtyId: Map } ).ptyTitleTrackersByPtyId runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) runtime.onPtyData('pty-1', '\x07', 100) expect(batches).toEqual([]) expect(trackerEntries.get('pty-1')?.commandCodeDetector).toBeNull() runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) runtime.onPtyData('pty-1', '\x07', 101) expect(batches.flatMap((batch) => batch.facts)).toEqual([{ kind: 'bell' }]) expect(trackerEntries.get('pty-1')?.commandCodeDetector).not.toBeNull() runtime.markGraphUnavailable(1) runtime.onPtyData('pty-1', '\x07', 102) expect(batches).toHaveLength(1) expect(trackerEntries.get('pty-1')?.commandCodeDetector).toBeNull() }) it('emits one batched event per chunk with facts in byte order and attribution', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) const chunk = '\x1b]0;Codex working\x07response\x1b]0;Codex done\x07\x07' runtime.onPtyData('pty-1', chunk, 100) expect(batches).toHaveLength(1) expect(batches[0]).toMatchObject({ ptyId: 'pty-1', seq: chunk.length, worktreeId: TEST_WORKTREE_ID, tabId: 'tab-1', paneKey: 'tab-1:1' }) expect(batches[0].replay).toBeUndefined() expect(batches[0].facts).toEqual([ { kind: 'title', normalizedTitle: 'Codex working', rawTitle: 'Codex working' }, { kind: 'agent-working' }, { kind: 'title', normalizedTitle: 'Codex done', rawTitle: 'Codex done' }, { kind: 'agent-idle', title: 'Codex done' }, { kind: 'bell' } ]) }) it('keeps per-PTY ordering across chunks and accumulates seq', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100) runtime.onPtyData('pty-1', '\x1b]0;Codex done\x07', 101) expect(batches.map((batch) => batch.facts[0]?.kind)).toEqual(['title', 'title']) expect(batches[0].seq).toBeLessThan(batches[1].seq) }) it('emits nothing for chunks without derived facts', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) // Plain output, a BEL-terminated non-title OSC split across chunks, and an Orca status payload — none is a title/bell/agent fact. runtime.onPtyData('pty-1', 'plain output\r\n', 100) runtime.onPtyData('pty-1', '\x1b]7;file://host', 101) runtime.onPtyData('pty-1', '/tmp\x07', 102) runtime.onPtyData('pty-1', '\x1b]9999;{"state":"working","agentType":"codex"}\x07', 103) expect(batches).toEqual([]) }) it('emits the stale-working-title rewrite as between-chunk fact batches', async () => { vi.useFakeTimers() try { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100) runtime.onPtyData('pty-1', 'output without a title\r\n', 101) batches.length = 0 await vi.advanceTimersByTimeAsync(3_000) // staleWorkingTitleClear tells the renderer to clear state without scheduling a task-complete notification the unthrottled timer didn't earn. expect(batches.flatMap((batch) => batch.facts)).toEqual([ { kind: 'title', normalizedTitle: 'Codex', rawTitle: 'Codex', staleWorkingTitleClear: true }, { kind: 'agent-idle', title: 'Codex', staleWorkingTitleClear: true } ]) } finally { vi.useRealTimers() } }) it('ingests synthetic title frames without touching the byte pipeline', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Cursor Agent\x07') expect(batches).toHaveLength(1) expect(batches[0].facts).toEqual([ { kind: 'title', normalizedTitle: '⠋ Cursor Agent', rawTitle: '⠋ Cursor Agent' }, // Synthesized spinner classifies as working — agent facts derive from synthetic frames the same as from real bytes. { kind: 'agent-working' } ]) // Synthetic frames are fabricated by main, so they must not advance the metered output sequence the renderer ACK budget uses. expect(runtime.getPtyOutputSequence('pty-1')).toBe(0) }) it('aligns a restored session and pre-response bytes to the provider sequence', async () => { const { runtime } = createSideEffectRuntime() // Simulate a stream-socket byte winning the race with the spawn response. runtime.onPtyData('pty-restored', 'queued', 100) expect( runtime.synchronizePtyOutputSequenceFromProvider( 'pty-restored', { value: 900, generation: 'continued' }, 0 ) ).toBe(906) runtime.onPtyData('pty-restored', 'fresh', 101) expect(runtime.getPtyOutputSequence('pty-restored')).toBe(911) await expect(runtime.serializeMainTerminalBuffer('pty-restored')).resolves.toMatchObject({ data: expect.stringContaining('queuedfresh'), seq: 911, source: 'headless' }) }) it('does not double a fresh daemon sequence on same-main reattach', () => { const { runtime } = createSideEffectRuntime() expect( runtime.synchronizePtyOutputSequenceFromProvider( 'pty-fresh', { value: 0, generation: 'reset' }, 0 ) ).toBe(0) runtime.onPtyData('pty-fresh', 'fresh output', 100) const sequenceBeforeReattach = runtime.getPtyOutputSequence('pty-fresh') expect( runtime.synchronizePtyOutputSequenceFromProvider( 'pty-fresh', { value: sequenceBeforeReattach, generation: 'continued' }, sequenceBeforeReattach ) ).toBe(sequenceBeforeReattach) }) it('does not jump ahead of delayed bytes covered by a reattach snapshot', () => { const { runtime } = createSideEffectRuntime() runtime.synchronizePtyOutputSequenceFromProvider( 'pty-delayed', { value: 0, generation: 'reset' }, 0 ) runtime.onPtyData('pty-delayed', 'before', 100) const sequenceBeforeReattach = runtime.getPtyOutputSequence('pty-delayed') const delayedCoveredBytes = 'queued' expect( runtime.synchronizePtyOutputSequenceFromProvider( 'pty-delayed', { value: sequenceBeforeReattach + delayedCoveredBytes.length, generation: 'continued' }, sequenceBeforeReattach ) ).toBe(sequenceBeforeReattach) runtime.onPtyData('pty-delayed', delayedCoveredBytes, 101) expect(runtime.getPtyOutputSequence('pty-delayed')).toBe( sequenceBeforeReattach + delayedCoveredBytes.length ) }) it('retains bytes emitted before a fresh daemon spawn resolves', async () => { const { runtime } = createSideEffectRuntime() const earlyOutput = '\x1b]0;Fresh shell\x07early prompt' runtime.onPtyData('pty-fresh', earlyOutput, 100) expect( runtime.synchronizePtyOutputSequenceFromProvider( 'pty-fresh', { value: 0, generation: 'reset' }, 0 ) ).toBe(earlyOutput.length) await expect(runtime.serializeMainTerminalBuffer('pty-fresh')).resolves.toMatchObject({ data: expect.stringContaining('early prompt'), lastTitle: 'Fresh shell', seq: earlyOutput.length }) }) it('drops stale headless state without rewinding the runtime sequence', async () => { const { runtime } = createSideEffectRuntime() runtime.synchronizePtyOutputSequenceFromProvider( 'pty-restarted', { value: 0, generation: 'reset' }, 0 ) runtime.onPtyData('pty-restarted', 'old generation', 100) const sequenceBeforeRespawn = runtime.getPtyOutputSequence('pty-restarted') expect( runtime.synchronizePtyOutputSequenceFromProvider( 'pty-restarted', { value: 0, generation: 'reset' }, sequenceBeforeRespawn ) ).toBe(sequenceBeforeRespawn) runtime.onPtyData('pty-restarted', 'new generation', 101) await expect(runtime.serializeMainTerminalBuffer('pty-restarted')).resolves.toMatchObject({ data: expect.not.stringContaining('old generation'), seq: sequenceBeforeRespawn + 'new generation'.length }) }) it('keeps active listener sequences monotonic across a daemon reset', () => { const { runtime } = createSideEffectRuntime() runtime.synchronizePtyOutputSequenceFromProvider( 'pty-reset', { value: 0, generation: 'reset' }, 0 ) runtime.onPtyData('pty-reset', 'old', 100) const sequenceBeforeRespawn = runtime.getPtyOutputSequence('pty-reset') const observedSequences: number[] = [] runtime.subscribeToTerminalData('pty-reset', (_data, meta) => { if (typeof meta?.seq === 'number') { observedSequences.push(meta.seq) } }) runtime.onPtyData('pty-reset', 'early', 101) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-reset', { value: 0, generation: 'reset' }, sequenceBeforeRespawn ) runtime.onPtyData('pty-reset', 'later', 102) expect(observedSequences).toEqual([ sequenceBeforeRespawn + 'early'.length, sequenceBeforeRespawn + 'early'.length + 'later'.length ]) }) it('carries the synthetic permission BEL as a bell fact', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;Cursor needs your input\x07\x07') expect(batches[0].facts.at(0)).toMatchObject({ kind: 'title' }) expect(batches[0].facts.at(-1)).toEqual({ kind: 'bell' }) }) it('emits command-finished facts with best-effort exit codes across chunk splits', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', 'output\x1b]133;D;13', 100) expect(batches).toEqual([]) runtime.onPtyData('pty-1', '0\x07prompt $ ', 101) runtime.onPtyData('pty-1', '\x1b]133;D\x07', 102) expect(batches.flatMap((batch) => batch.facts)).toEqual([ { kind: 'command-finished', exitCode: 130 }, { kind: 'command-finished', exitCode: null } ]) }) it('emits pr-link facts once per URL with batch attribution', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', 'PR https://github.com/acme/orca/pull/4', 100) runtime.onPtyData('pty-1', '2\r\nand https://github.com/acme/orca/pull/43 done\r\n', 101) // Repeated URL: deduped per PTY, like the renderer byte detector. runtime.onPtyData('pty-1', 'again https://github.com/acme/orca/pull/42\r\n', 102) expect(batches).toHaveLength(1) expect(batches[0]).toMatchObject({ ptyId: 'pty-1', worktreeId: TEST_WORKTREE_ID, tabId: 'tab-1' }) expect(batches[0].facts).toEqual([ { kind: 'pr-link', link: { url: 'https://github.com/acme/orca/pull/42', slug: { owner: 'acme', repo: 'orca', host: 'github.com' }, number: 42 } }, { kind: 'pr-link', link: { url: 'https://github.com/acme/orca/pull/43', slug: { owner: 'acme', repo: 'orca', host: 'github.com' }, number: 43 } } ]) }) it('emits 2031-subscribe facts across chunk splits', () => { // Why: hidden-delivery-gated views never get the bytes, so this fact is their only cue to send the DECSET 2031 color-scheme reply. const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b[?20', 100) expect(batches).toEqual([]) runtime.onPtyData('pty-1', '31h', 101) expect(batches.flatMap((batch) => batch.facts)).toEqual([{ kind: '2031-subscribe' }]) }) it('prefers the tracked title over the renderer snapshot lastTitle', async () => { const { runtime } = createSideEffectRuntime() const serializeBuffer = vi.fn().mockResolvedValue({ data: 'visible content', cols: 80, rows: 24, // Renderer xterm never saw the synthetic frame (no longer rides pty:data), so its serializer reports a stale title. lastTitle: 'stale shell title' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, hasRendererSerializer: () => true, getSize: () => ({ cols: 80, rows: 24 }) }) syncSinglePty(runtime) runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Cursor Agent\x07') const snapshot = await runtime.serializeTerminalBuffer('pty-1', { scrollbackRows: 10 }) expect(snapshot?.source).toBe('renderer') expect(snapshot?.lastTitle).toBe('⠋ Cursor Agent') }) it('falls back to the provider snapshot for a restored PTY with no mounted renderer', async () => { const { runtime } = createSideEffectRuntime() const serializeBuffer = vi.fn() const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: 'restored screen\r\n', scrollbackAnsi: 'restored history\r\n', cols: 120, rows: 40, cwd: '/projects/restored', seq: 900, source: 'headless' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, serializeProviderBuffer, hasRendererSerializer: () => false }) const snapshot = await runtime.serializeTerminalBuffer('pty-restored', { scrollbackRows: 5000 }) expect(serializeBuffer).not.toHaveBeenCalled() expect(serializeProviderBuffer).toHaveBeenCalledWith('pty-restored', { scrollbackRows: 5000 }) expect(snapshot).toEqual({ data: 'restored screen\r\n', scrollbackAnsi: 'restored history\r\n', cols: 120, rows: 40, cwd: '/projects/restored', seq: 900, source: 'headless' }) }) it('falls back to provider history when a mounted renderer has not hydrated yet', async () => { const { runtime } = createSideEffectRuntime() const serializeBuffer = vi.fn().mockResolvedValue({ data: '', cols: 80, rows: 24 }) const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: '', scrollbackAnsi: 'restored history\r\n', cols: 120, rows: 40, seq: 900, source: 'headless' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, serializeProviderBuffer, hasRendererSerializer: () => true }) const snapshot = await runtime.serializeTerminalBuffer('pty-restored', { scrollbackRows: 5000 }) expect(serializeBuffer).toHaveBeenCalledOnce() expect(serializeProviderBuffer).toHaveBeenCalledWith('pty-restored', { scrollbackRows: 5000 }) expect(snapshot).toMatchObject({ data: '', scrollbackAnsi: 'restored history\r\n', source: 'headless' }) }) it('keeps an empty renderer snapshot when the provider has no retained content', async () => { const { runtime } = createSideEffectRuntime() const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: '', scrollbackAnsi: '', cols: 120, rows: 40, seq: 0, source: 'headless' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer: vi.fn().mockResolvedValue({ data: '', cols: 51, rows: 40 }), serializeProviderBuffer, hasRendererSerializer: () => true }) const snapshot = await runtime.serializeTerminalBuffer('pty-new') expect(serializeProviderBuffer).toHaveBeenCalledOnce() expect(snapshot).toMatchObject({ data: '', cols: 51, rows: 40, source: 'renderer' }) }) it('does not let pre-response bytes hide restored provider history', async () => { const { runtime } = createSideEffectRuntime() const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: 'restored history\r\nqueued', cols: 80, rows: 24, seq: 906, source: 'headless' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) runtime.onPtyData('pty-restored', 'queued', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-restored', { value: 900, generation: 'continued' }, 0 ) const snapshot = await runtime.serializeTerminalBuffer('pty-restored', { scrollbackRows: 5000 }) expect(snapshot?.data).toContain('restored history') expect(serializeProviderBuffer).toHaveBeenCalledOnce() }) it('keeps restored provider history authoritative after later live output', async () => { const { runtime } = createSideEffectRuntime() const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: 'restored history\r\nlater output', cols: 80, rows: 24, seq: 912, source: 'headless' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-restored', { value: 900, generation: 'continued' }, 0 ) runtime.onPtyData('pty-restored', 'later output', 100) const snapshot = await runtime.serializeTerminalBuffer('pty-restored') expect(snapshot?.data).toContain('restored history') expect(serializeProviderBuffer).toHaveBeenCalledOnce() }) it('uses provider alternate-screen state while a partial model is unsafe', async () => { const { runtime } = createSideEffectRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer: vi.fn().mockResolvedValue({ data: 'restored tui', cols: 80, rows: 24, seq: 903, source: 'headless', alternateScreen: true }), hasRendererSerializer: () => false }) runtime.onPtyData('pty-tui', 'tui', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-tui', { value: 900, generation: 'continued' }, 0 ) await runtime.serializeTerminalBuffer('pty-tui') expect(runtime.isTerminalAlternateScreen('pty-tui')).toBe(true) }) it('tracks live alternate-screen transitions after a provider snapshot', async () => { const { runtime } = createSideEffectRuntime() const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: 'restored tui', cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: true }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-tui', { value: 900, generation: 'continued' }, 0 ) await runtime.serializeTerminalBuffer('pty-tui') expect(runtime.isTerminalAlternateScreen('pty-tui')).toBe(true) runtime.onPtyData('pty-tui', '\x1b[?1049l', 100) expect(runtime.isTerminalAlternateScreen('pty-tui')).toBe(false) runtime.onPtyData('pty-tui', '\x1b[?1049h', 101) expect(runtime.isTerminalAlternateScreen('pty-tui')).toBe(true) }) it('keeps mode transitions that race a provider snapshot response', async () => { const { runtime } = createSideEffectRuntime() let resolveProviderSnapshot: | ((snapshot: { data: string cols: number rows: number seq: number source: 'headless' alternateScreen: boolean }) => void) | undefined const serializeProviderBuffer = vi.fn( () => new Promise<{ data: string cols: number rows: number seq: number source: 'headless' alternateScreen: boolean }>((resolve) => { resolveProviderSnapshot = resolve }) ) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-tui-race', { value: 900, generation: 'continued' }, 0 ) const snapshotPromise = runtime.serializeTerminalBuffer('pty-tui-race') await vi.waitFor(() => expect(resolveProviderSnapshot).toBeDefined()) runtime.onPtyData('pty-tui-race', '\x1b[?1049l', 100) resolveProviderSnapshot?.({ data: 'captured alt screen', cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: true }) await snapshotPromise expect(runtime.isTerminalAlternateScreen('pty-tui-race')).toBe(false) }) it('translates reset provider snapshots without retaining the old title', async () => { const { runtime } = createSideEffectRuntime() runtime.synchronizePtyOutputSequenceFromProvider( 'pty-replaced', { value: 0, generation: 'reset' }, 0 ) runtime.onPtyData('pty-replaced', '\x1b]0;Old process\x07old', 100) const sequenceBeforeRespawn = runtime.getPtyOutputSequence('pty-replaced') runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer: vi.fn().mockResolvedValue({ data: 'new process', cols: 80, rows: 24, seq: 'new process'.length, source: 'headless', lastTitle: 'New process' }), hasRendererSerializer: () => false }) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-replaced', { value: 0, generation: 'reset' }, sequenceBeforeRespawn ) await expect(runtime.serializeTerminalBuffer('pty-replaced')).resolves.toMatchObject({ lastTitle: 'New process', seq: sequenceBeforeRespawn + 'new process'.length }) }) it('prefers the tracked title over the headless emulator lastTitle', async () => { const { runtime } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07real output\r\n', 100) // Hook-driven idle frame lands only in main's tracker; the emulator never sees fabricated bytes (invariant 5). runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;Codex ready\x07') const snapshot = await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 10 }) expect(snapshot?.source).toBe('headless') expect(snapshot?.lastTitle).toBe('Codex ready') }) it('returns a title-only replay snapshot and never historical attention', () => { const { runtime } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07\x07', 100) expect(runtime.getTerminalSideEffectSnapshot('pty-1')).toMatchObject({ ptyId: 'pty-1', replay: true, facts: [{ kind: 'title', normalizedTitle: 'Codex working', rawTitle: 'Codex working' }] }) expect(runtime.getTerminalSideEffectSnapshot('pty-unknown')).toBeNull() }) it('drops the cursor-agent literal from record-fallback snapshots', () => { const { runtime } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', 'plain output\n', 100) // Simulate a record title restored by a path that bypassed the tracker (which itself refuses to store the bare native title). const records = ( runtime as unknown as { ptysById: Map } ).ptysById records.get('pty-1')!.lastOscTitle = 'Cursor Agent' expect(runtime.getTerminalSideEffectSnapshot('pty-1')).toBeNull() }) it('emits the chunk agentStatus events before its side-effect batch', () => { // Cross-channel contract order per chunk: status → titles → bell. const order: string[] = [] const runtime = new OrcaRuntimeService(store, undefined, { onTerminalAgentStatus: () => order.push('agentStatus:set'), onTerminalSideEffects: () => order.push('pty:sideEffect') }) syncSinglePty(runtime) runtime.onPtyData( 'pty-1', '\x1b]9999;{"state":"working","agentType":"codex"}\x07\x1b]0;Codex working\x07\x07', 100 ) expect(order).toEqual(['agentStatus:set', 'pty:sideEffect']) }) it('still emits a throwing chunk’s facts under its own seq, not the next chunk’s', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) vi.spyOn( runtime as unknown as { applyTrackedPtyTitle: (ptyId: string, title: string) => boolean }, 'applyTrackedPtyTitle' ).mockImplementationOnce(() => { throw new Error('tracker boom') }) const first = '\x1b]0;Codex working\x07' expect(() => runtime.onPtyData('pty-1', first, 100)).toThrow('tracker boom') runtime.onPtyData('pty-1', '\x1b]0;Codex done\x07', 101) expect(batches).toHaveLength(2) expect(batches[0].seq).toBe(first.length) expect(batches[0].facts).toEqual([ { kind: 'title', normalizedTitle: 'Codex working', rawTitle: 'Codex working' } ]) // Next chunk's batch carries only its own facts: the throw aborted the first chunk's tracker pass, so no working state kept. expect(batches[1].seq).toBeGreaterThan(batches[0].seq) expect(batches[1].facts).toEqual([ { kind: 'title', normalizedTitle: 'Codex done', rawTitle: 'Codex done' } ]) }) it('parses synthetic frames statelessly so ticks cannot corrupt the bell detector', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;split ti', 100) // An 80ms spinner tick lands between the two halves of the real OSC. runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Cursor Agent\x07') // Continuation: this BEL terminates the real OSC — it is NOT a bell. runtime.onPtyData('pty-1', 'tle\x07', 101) // A later standalone BEL is a real bell and must not be swallowed. runtime.onPtyData('pty-1', 'ready\x07', 102) expect(batches.flatMap((batch) => batch.facts)).toEqual([ { kind: 'title', normalizedTitle: '⠋ Cursor Agent', rawTitle: '⠋ Cursor Agent' }, { kind: 'agent-working' }, { kind: 'title', normalizedTitle: 'split title', rawTitle: 'split title' }, { kind: 'bell' } ]) }) it('touches mobile snapshots once for decorative spinner ticks, again on idle', () => { const { runtime } = createSideEffectRuntime() syncSinglePty(runtime) const touchSpy = vi.spyOn( runtime as unknown as { touchMobileSessionSnapshotsForPty: (ptyId: string) => void }, 'touchMobileSessionSnapshotsForPty' ) for (const frame of ['⠋', '⠙', '⠹', '⠸', '⠼']) { runtime.ingestSyntheticTitleFrame('pty-1', `\x1b]0;${frame} Cursor Agent\x07`) } // Five ticks with the same de-spinnered title: one snapshot fan-out. expect(touchSpy).toHaveBeenCalledTimes(1) runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;Cursor ready\x07') expect(touchSpy).toHaveBeenCalledTimes(2) // Raw record titles still track every frame for worktree ps/mobile tabs. expect( ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1')?.lastOscTitle ).toBe('Cursor ready') }) it('seeds the lazily created tracker from the daemon-snapshot title', async () => { const { runtime, batches } = createSideEffectRuntime() const serializeBuffer = vi.fn().mockResolvedValue({ data: 'restored scrollback\n', cols: 80, rows: 24, lastTitle: 'Codex working' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, hasRendererSerializer: () => true, getSize: () => ({ cols: 80, rows: 24 }) }) syncSinglePty(runtime) // First live chunk creates the tracker cold and kicks off hydration; the snapshot seed must land in that tracker. runtime.onPtyData('pty-1', 'plain output without a title\n', 100) await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 10 }) batches.length = 0 runtime.onPtyData('pty-1', '\x1b]0;Codex done\x07', 101) // Without the seed the tracker never saw 'working', so this idle title could not produce a completion fact. expect(batches.flatMap((batch) => batch.facts)).toContainEqual({ kind: 'agent-idle', title: 'Codex done' }) }) it('arms the stale-title timer for a seeded working title', async () => { vi.useFakeTimers() try { const { runtime, batches } = createSideEffectRuntime() const serializeBuffer = vi.fn().mockResolvedValue({ data: 'restored scrollback\n', cols: 80, rows: 24, lastTitle: 'Codex working' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, hasRendererSerializer: () => true, getSize: () => ({ cols: 80, rows: 24 }) }) syncSinglePty(runtime) runtime.onPtyData('pty-1', 'plain output\n', 100) // Settle the async daemon-snapshot hydration that seeds the tracker. await vi.advanceTimersByTimeAsync(0) runtime.onPtyData('pty-1', 'still no title\n', 101) batches.length = 0 await vi.advanceTimersByTimeAsync(3_000) expect(batches.flatMap((batch) => batch.facts)).toEqual([ { kind: 'title', normalizedTitle: 'Codex', rawTitle: 'Codex', staleWorkingTitleClear: true }, { kind: 'agent-idle', title: 'Codex', staleWorkingTitleClear: true } ]) } finally { vi.useRealTimers() } }) it('emits command-code-working facts only after the banner arms the scrape', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) // Generic status words without the Command Code banner must not arm. runtime.onPtyData('pty-1', '❯ Fix the spinner\r\nThinking...', 100) expect(batches.flatMap((batch) => batch.facts)).toEqual([]) runtime.onPtyData('pty-1', '# Command Code v0.27.3\r\n', 101) runtime.onPtyData('pty-1', '❯ Fix the spinner\r\n\x1b[35m✻ Thinking...\x1b[0m', 102) expect(batches.at(-1)).toMatchObject({ ptyId: 'pty-1', worktreeId: TEST_WORKTREE_ID, tabId: 'tab-1' }) expect(batches.at(-1)?.facts).toEqual([ { kind: 'command-code-working', prompt: 'Fix the spinner' } ]) }) it('emits a command-code-done fact when the idle composer returns', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) runtime.onPtyData('pty-1', '# Command Code v0.27.3\r\n', 100) runtime.onPtyData('pty-1', '❯ say hi\r\n✻ Thinking...', 101) runtime.onPtyData( 'pty-1', '\r\n✻ Thought for 1 second\r\n:: Hi!\r\n❯ Ask your question...', 102 ) expect(batches.at(-1)?.facts).toEqual([{ kind: 'command-code-done', prompt: 'say hi' }]) }) it('arms the Command Code scrape from the noted spawn command', () => { const { runtime, batches } = createSideEffectRuntime() syncSinglePty(runtime) // Mirrors the renderer detector's startupCommand fast-arm: no banner needed when main saw the launch command at spawn. runtime.noteTerminalSpawnCommand('pty-1', 'command-code --trust') runtime.onPtyData('pty-1', '❯ Fix the spinner\r\n✻ Thinking...', 100) expect(batches.flatMap((batch) => batch.facts)).toContainEqual({ kind: 'command-code-working', prompt: 'Fix the spinner' }) }) it('prefers the tracked title over a stale renderer lastTitle in the hydration seed', async () => { const { runtime } = createSideEffectRuntime() const serializeBuffer = vi.fn().mockResolvedValue({ data: 'renderer scrollback\n', cols: 80, rows: 24, // Renderer xterm never saw the synthetic hook frame (no longer rides pty:data), so its serializer reports the pre-agent title. lastTitle: 'stale shell title' }) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, hasRendererSerializer: () => true, getSize: () => ({ cols: 80, rows: 24 }) }) syncSinglePty(runtime) runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Claude working\x07') // First live chunk kicks off renderer hydration; awaiting the snapshot below settles the seed write chain. runtime.onPtyData('pty-1', 'plain output\n', 100) await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 10 }) const leaves = ( runtime as unknown as { leaves: Map } ).leaves // The seed must not stomp the leaf record (worktree ps status source) back to the renderer's stale title. expect([...leaves.values()][0]?.lastOscTitle).toBe('⠋ Claude working') }) }) it('returns OSC titles from headless main terminal snapshots', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07hello\n', 100) const snapshot = await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 1000 }) expect(snapshot).toMatchObject({ source: 'headless', lastTitle: 'Codex working' }) }) it('resizes the headless mirror after an accepted desktop PTY resize', async () => { const spawn = { cols: 80, rows: 24 } const resized = { cols: 120, rows: 30 } let currentSize = spawn const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, getSize: () => currentSize }) syncSinglePty(runtime, 'pty-1') runtime.onPtyData('pty-1', 'user@host % claude\r\n', 100) currentSize = resized runtime.onExternalPtyResize('pty-1', resized.cols, resized.rows) for (let index = 0; index < 5; index += 1) { runtime.onPtyData('pty-1', makeStatusFrame(index, index === 0), 200 + index) } const snapshot = await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 5000 }) expect(snapshot).toMatchObject({ cols: resized.cols, rows: resized.rows, source: 'headless' }) await expect(parseHeadlessSnapshotLines(snapshot!, resized)).resolves.toEqual( await referenceStatusFrameLines(spawn, resized) ) }) it('orders headless mirror resizes behind queued PTY writes', async () => { const spawn = { cols: 80, rows: 10 } const resized = { cols: 120, rows: 10 } let currentSize = spawn const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, getSize: () => currentSize, resize: () => { currentSize = resized return true } }) syncSinglePty(runtime, 'pty-1') runtime.onPtyData('pty-1', 'prompt\r\n', 100) await runtime.serializeMainTerminalBuffer('pty-1') type HeadlessStateForTest = { emulator: HeadlessEmulator writeChain: Promise } const headless = ( runtime as unknown as { headlessTerminals: Map } ).headlessTerminals.get('pty-1') expect(headless).toBeDefined() const originalWrite = headless!.emulator.write.bind(headless!.emulator) const queuedWriteStarted = makeDeferred() const releaseQueuedWrite = makeDeferred() headless!.emulator.write = async (data: string): Promise => { queuedWriteStarted.resolve() await releaseQueuedWrite.promise await originalWrite(data) } try { runtime.onPtyData('pty-1', '\x1b[90GOLD', 200) await queuedWriteStarted.promise await runtime.updateDesktopViewport('pty-1', resized) runtime.onPtyData('pty-1', '\r\nNEXT', 300) releaseQueuedWrite.resolve() const snapshot = await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 100 }) expect(snapshot).toMatchObject({ cols: resized.cols, rows: resized.rows }) await expect(parseHeadlessSnapshotLines(snapshot!, resized)).resolves.toEqual([ 'prompt', ' O', 'LD', 'NEXT' ]) } finally { headless!.emulator.write = originalWrite releaseQueuedWrite.resolve() } }) it('adopts renderer-seeded titles into headless main terminal snapshots', async () => { const artifactPath = '/tmp/renderer-seeded-artifact.json' const serializeBuffer = vi.fn().mockResolvedValue({ data: `renderer scrollback\nwrote ${artifactPath}\n`, cols: 100, rows: 30, lastTitle: 'Renderer seeded Codex' }) const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, hasRendererSerializer: () => true, getSize: () => ({ cols: 100, rows: 30 }) }) syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'live output without title\n', 100) const snapshot = await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 1000 }) expect(snapshot).toMatchObject({ source: 'headless', lastTitle: 'Renderer seeded Codex' }) expect(serializeBuffer).toHaveBeenCalledWith('pty-1', { scrollbackRows: expect.any(Number), altScreenForcesZeroRows: true }) expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('returns cwd metadata from seeded headless main terminal snapshots', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/restored-scrollback-artifact.json' runtime.seedHeadlessTerminal( 'pty-1', `restored scrollback\nwrote ${artifactPath}\n`, { cols: 100, rows: 30 }, { cwd: '/projects/restored' } ) const snapshot = await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 1000 }) expect(snapshot).toMatchObject({ source: 'headless', cwd: '/projects/restored' }) expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('resolves paths without candidate activation via the lazy safety net', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/lazy-activation-artifact.json' runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100) // No mobile connect ever happened; the query itself must activate+backfill. expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('backfills candidates on activation so scrolled-off paths still resolve', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/backfilled-artifact.json' // Path arrives while tracking is inactive (desktop-only phase). runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100) // First mobile connect: backfill from the retained raw window. runtime.activateRecentPtyPathCandidateTracking() // Scroll the raw 64KB window past the path with pathless output. runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) // Only the backfilled candidate tier can answer now. expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('backfills per retained chunk so chunk boundaries match the eager extractor', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/a.json' // Two chunks whose join would parse as one different candidate // (/tmp/a.jsonsuffix.txt). The eager per-chunk extractor kept /tmp/a.json. runtime.onPtyData('pty-1', `wrote ${artifactPath}`, 100) runtime.onPtyData('pty-1', 'suffix.txt', 150) runtime.activateRecentPtyPathCandidateTracking() // Scroll the raw 64KB window so only the backfilled candidates can answer. runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('extracts candidates per chunk after activation for scrolled-off paths', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/post-activation-artifact.json' runtime.activateRecentPtyPathCandidateTracking() // Idempotent: a second activation must not disturb live tracking. runtime.activateRecentPtyPathCandidateTracking() runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100) runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('does not retain pre-activation paths that scrolled past the raw window', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/pre-activation-scrolled-artifact.json' runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100) runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) // Documented accepted loss: output that scrolled past the raw window // before the first-ever mobile connect yields no candidates. expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( false ) }) it('backfill does not mint candidates from an over-limit line shortened by the window trim', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/result.json' // One chunk with a >4KiB line whose tail is the path: the eager // extractor skipped it under the line-length guard. runtime.onPtyData('pty-1', `${'a'.repeat(5000)} ${artifactPath}\n`, 100) // Newline-free filler trims the window to ~1KiB before the path, so a // trimmed-head replay would see an under-limit line ending in the path. runtime.onPtyData('pty-1', 'y'.repeat(64 * 1024 - 1000), 150) runtime.activateRecentPtyPathCandidateTracking() // Scroll the raw window so only backfilled candidates can answer. runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) // Parity with eager extraction: the over-limit line never yielded a // candidate, so the grant must stay denied after the raw window scrolls. expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( false ) }) it('backfill replays the full head chunk including its window-trimmed prefix', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/trimmed-prefix-artifact.json' // Path sits in the head chunk's prefix, which the window trim drops from // read() but the eager extractor saw at append time. runtime.onPtyData('pty-1', `wrote ${artifactPath}\n${'b'.repeat(3000)}\n`, 100) runtime.onPtyData('pty-1', 'y'.repeat(64 * 1024 - 1000), 150) runtime.activateRecentPtyPathCandidateTracking() runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) // Parity with eager extraction: the append-time candidate outlived the // raw window, so backfill must recover it from the intact head chunk. expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('matches eager extraction exactly for a pre-sliced oversized chunk', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const cutLinePath = '/tmp/cut-line.json' const keptPath = '/tmp/kept-after-cut.json' // Single >64KiB append is stored pre-sliced, so its original text is // unrecoverable at activation time. Extraction runs eagerly at append // instead: cutLinePath sat on an over-4KiB line the extractor's line // guard rejects (and the slice leaves an under-4KiB tail of it that must // NOT mint a candidate later), while keptPath sat on a short line and // must survive the raw window scrolling. const keptLine = `wrote ${keptPath}\n` const afterFirstLine = `${keptLine}${'z'.repeat(62 * 1024 - keptLine.length)}` const oversized = `${'a'.repeat(5 * 1024)} ${cutLinePath}\n${afterFirstLine}` runtime.onPtyData('pty-1', oversized, 100) runtime.activateRecentPtyPathCandidateTracking() runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) expect(runtime.hasRecentTerminalOutputPath(terminal.handle, cutLinePath, cutLinePath)).toBe( false ) expect(runtime.hasRecentTerminalOutputPath(terminal.handle, keptPath, keptPath)).toBe(true) }) it('keeps a candidate from the short first line of an oversized chunk after the window scrolls', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals const artifactPath = '/tmp/result.json' // A short first line of a >64KiB chunk loses only its `wrote ` prefix to // the pre-slice; the path itself stays in the retained window. The old // eager extractor recorded it from the intact original chunk, so it must // stay authorized after the raw window scrolls — parity requires the // append-time extraction for oversized chunks, not backfill replay. const firstLine = `wrote ${artifactPath}\n` runtime.onPtyData('pty-1', `${firstLine}${'f'.repeat(64 * 1024 + 6 - firstLine.length)}`, 100) runtime.activateRecentPtyPathCandidateTracking() runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) expect(runtime.hasRecentTerminalOutputPath(terminal.handle, artifactPath, artifactPath)).toBe( true ) }) it('replaces suffix-only headless state with the recovered renderer snapshot', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') runtime.seedHeadlessTerminal('pty-1', 'suffix-only redraw', { cols: 80, rows: 24 }) runtime.replaceHeadlessTerminalFromRendererSnapshotForRecovery('pty-1', { data: 'restored history\r\nprompt $ ', cols: 80, rows: 24, cwd: '/projects/restored' }) runtime.onPtyData('pty-1', 'after recovery\r\n', 100) const snapshot = await runtime.serializeMainTerminalBuffer('pty-1', { scrollbackRows: 100 }) expect(snapshot?.data).toContain('restored history') expect(snapshot?.data).toContain('after recovery') expect(snapshot?.data).not.toContain('suffix-only redraw') expect(snapshot?.cwd).toBe('/projects/restored') }) it('adopts OSC7 host metadata from seeded headless terminal scrollback', async () => { const runtime = createRuntime() syncSinglePty(runtime, 'pty-1') const [terminal] = (await runtime.listTerminals()).terminals runtime.seedHeadlessTerminal( 'pty-1', '\x1b]7;file://remote-host/tmp\x07restored scrollback\n', { cols: 100, rows: 30 } ) expect(runtime.resolveTerminalFileUriHostname(terminal.handle)).toBe('remote-host') }) it('falls back to the renderer snapshot for hidden-output recovery without headless state', async () => { const serializeBuffer = vi.fn().mockResolvedValue({ data: '\x1b[?1049hRenderer TUI\r\nStill running\r\n', cols: 100, rows: 30, lastTitle: 'Renderer working' }) const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, hasRendererSerializer: () => true }) syncSinglePty(runtime, 'pty-1') const snapshot = await runtime.serializeHiddenOutputRecoveryBuffer('pty-1', { scrollbackRows: 5000 }) expect(snapshot).toEqual({ data: '\x1b[?1049hRenderer TUI\r\nStill running\r\n', cols: 100, rows: 30, lastTitle: 'Renderer working', source: 'renderer' }) expect(serializeBuffer).toHaveBeenCalledWith('pty-1', { scrollbackRows: 5000, altScreenForcesZeroRows: false }) }) it('keeps an empty headless snapshot authoritative for hidden-output recovery', async () => { const serializeBuffer = vi.fn().mockResolvedValue({ data: 'stale renderer content\r\n', cols: 80, rows: 24 }) const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, hasRendererSerializer: () => true }) type HeadlessStateForTest = { emulator: { isAlternateScreen: boolean getSnapshot: (opts: { scrollbackRows?: number }) => { rehydrateSequences: string snapshotAnsi: string cols: number rows: number } } outputSequence: number writeChain: Promise } const runtimePrivate = runtime as unknown as { headlessTerminals: Map } runtimePrivate.headlessTerminals.set('pty-empty', { emulator: { isAlternateScreen: false, getSnapshot: () => ({ rehydrateSequences: '', snapshotAnsi: '', cols: 90, rows: 30 }) }, outputSequence: 17, writeChain: Promise.resolve() }) await expect(runtime.serializeHiddenOutputRecoveryBuffer('pty-empty')).resolves.toEqual({ data: '', cols: 90, rows: 30, seq: 17, source: 'headless', // Non-alt-screen reports alternateScreen=false so the renderer keeps its destructive scrollback clear on restore. alternateScreen: false }) expect(serializeBuffer).not.toHaveBeenCalled() }) it('advances the absolute output sequence across a daemon stream gap', () => { const runtime = createRuntime() runtime.onPtyData('pty-gap', 'before', Date.now()) runtime.notePtyDataGap('pty-gap', 4096) runtime.onPtyData('pty-gap', 'after', Date.now()) expect(runtime.getPtyOutputSequence('pty-gap')).toBe('before'.length + 4096 + 'after'.length) }) it('emits explicit OSC 9999 agent status from runtime PTY data', () => { const statuses: RuntimeTerminalAgentStatusEvent[] = [] const runtime = new OrcaRuntimeService(store, undefined, { onTerminalAgentStatus: (event) => statuses.push(event) }) const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = `tab-1:${leafId}` runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData( 'pty-1', 'before\x1b]9999;{"state":"working","prompt":"ship it","agentType":"codex"}\x07after', 123 ) expect(statuses).toEqual([ { ptyId: 'pty-1', source: 'mounted-leaf', paneKey, tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, connectionId: null, payload: { state: 'working', prompt: 'ship it', agentType: 'codex' } } ]) }) it('stamps SSH connection identity on runtime terminal status', () => { const statuses: RuntimeTerminalAgentStatusEvent[] = [] const runtime = new OrcaRuntimeService(store, undefined, { onTerminalAgentStatus: (event) => statuses.push(event) }) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-ssh' } ] }) runtime.registerPty('pty-ssh', TEST_WORKTREE_ID, 'ssh-conn-1') runtime.onPtyData('pty-ssh', '\x1b]9999;{"state":"working","agentType":"codex"}\x07', 123) expect(statuses).toEqual([ expect.objectContaining({ ptyId: 'pty-ssh', source: 'mounted-leaf', connectionId: 'ssh-conn-1', payload: expect.objectContaining({ state: 'working', agentType: 'codex' }) }) ]) }) it('keeps SSH OSC7 cwd POSIX when the desktop runtime is on Windows', async () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) runtime.preparePtyExecutionContext('pty-ssh', 'Ubuntu', { resetIncarnation: true }) runtime.registerPty('pty-ssh', TEST_WORKTREE_ID, 'ssh-conn-1') runtime.onPtyData('pty-ssh', '\x1b]7;file://remote-host/home/me/repo/src\x07', 123) const internals = runtime as unknown as { terminalCwdByPtyId: Map terminalFileUriHostnameByPtyId: Map wslDistroByPtyId: Map } expect(internals.terminalCwdByPtyId.get('pty-ssh')).toBe('/home/me/repo/src') expect(internals.terminalFileUriHostnameByPtyId.get('pty-ssh')).toBe('remote-host') expect(internals.wslDistroByPtyId.has('pty-ssh')).toBe(false) }) it('uses per-incarnation WSL context before registration and across simultaneous distros', () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) runtime.preparePtyExecutionContext('pty-ubuntu', 'Ubuntu', { resetIncarnation: true }) runtime.preparePtyExecutionContext('pty-debian', 'Debian', { resetIncarnation: true }) runtime.registerPty('pty-ubuntu', TEST_WORKTREE_ID) runtime.registerPty('pty-debian', TEST_WORKTREE_ID) runtime.onPtyData('pty-ubuntu', '\x1b]7;file://DESKTOP/home/me/repo\x07', 1) runtime.onPtyData('pty-debian', '\x1b]7;file://DESKTOP/home/me/repo\x07', 1) const cwds = (runtime as unknown as { terminalCwdByPtyId: Map }) .terminalCwdByPtyId expect(cwds.get('pty-ubuntu')).toBe('\\\\wsl.localhost\\Ubuntu\\home\\me\\repo') expect(cwds.get('pty-debian')).toBe('\\\\wsl.localhost\\Debian\\home\\me\\repo') }) it('does not retain WSL context when a PTY id is reused', () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) runtime.preparePtyExecutionContext('pty-reused', 'Ubuntu', { resetIncarnation: true }) runtime.registerPty('pty-reused', TEST_WORKTREE_ID) runtime.onPtyExit('pty-reused', 0) runtime.preparePtyExecutionContext('pty-reused', null, { resetIncarnation: true }) runtime.registerPty('pty-reused', TEST_WORKTREE_ID) runtime.onPtyData('pty-reused', '\x1b]7;file://server/share/repo\x07', 1) const cwds = (runtime as unknown as { terminalCwdByPtyId: Map }) .terminalCwdByPtyId expect(cwds.get('pty-reused')).toBe('\\\\server\\share\\repo') }) it('preserves immutable context while a live daemon attach is unresolved', () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) runtime.preparePtyExecutionContext('pty-attached', 'Ubuntu', { resetIncarnation: true }) runtime.registerPty('pty-attached', TEST_WORKTREE_ID) const changed = runtime.preparePtyExecutionContext('pty-attached', 'Debian', { preserveExisting: true }) runtime.onPtyData('pty-attached', '\x1b]7;file://DESKTOP/home/me/repo\x07', 1) const cwd = ( runtime as unknown as { terminalCwdByPtyId: Map } ).terminalCwdByPtyId.get('pty-attached') expect(changed).toBe(false) expect(cwd).toBe('\\\\wsl.localhost\\Ubuntu\\home\\me\\repo') }) it('replaces a cwd parsed before late WSL context with the provider cwd', async () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) const ptyId = 'pty-late-wsl-context' const providerCwd = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo' runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, getSize: () => ({ cols: 80, rows: 24 }), serializeProviderBuffer: vi.fn().mockResolvedValue({ data: 'restored screen', cols: 80, rows: 24, cwd: providerCwd, seq: 1, source: 'headless' }) }) runtime.registerPty(ptyId, TEST_WORKTREE_ID) runtime.seedHeadlessTerminal(ptyId, '\x1b]7;file://DESKTOP/home/me/repo\x07') const internals = runtime as unknown as { headlessTerminals: Map }> terminalCwdByPtyId: Map } await internals.headlessTerminals.get(ptyId)?.writeChain expect(internals.terminalCwdByPtyId.get(ptyId)).toBe('\\\\desktop\\home\\me\\repo') runtime.preparePtyExecutionContext(ptyId, 'Ubuntu') await internals.headlessTerminals.get(ptyId)?.writeChain expect(internals.terminalCwdByPtyId.get(ptyId)).toBe(providerCwd) }) it('keeps a live WSL cwd that arrives during late-context snapshot recovery', async () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) const ptyId = 'pty-late-wsl-context-race' type ProviderSnapshot = { data: string cols: number rows: number cwd: string seq: number source: 'headless' } let resolveProviderSnapshot: ((snapshot: ProviderSnapshot) => void) | undefined runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, getSize: () => ({ cols: 80, rows: 24 }), serializeProviderBuffer: vi.fn( () => new Promise((resolve) => { resolveProviderSnapshot = resolve }) ) }) runtime.registerPty(ptyId, TEST_WORKTREE_ID) runtime.seedHeadlessTerminal(ptyId, '\x1b]7;file://DESKTOP/home/me/old\x07') const internals = runtime as unknown as { headlessTerminals: Map }> terminalCwdByPtyId: Map } await internals.headlessTerminals.get(ptyId)?.writeChain runtime.preparePtyExecutionContext(ptyId, 'Ubuntu') await vi.waitFor(() => expect(resolveProviderSnapshot).toBeDefined()) runtime.onPtyData(ptyId, '\x1b]7;file://DESKTOP/home/me/live\x07', 1) resolveProviderSnapshot?.({ data: 'older restored screen', cols: 80, rows: 24, cwd: '\\\\wsl.localhost\\Ubuntu\\home\\me\\old', seq: 1, source: 'headless' }) await internals.headlessTerminals.get(ptyId)?.writeChain expect(internals.terminalCwdByPtyId.get(ptyId)).toBe( '\\\\wsl.localhost\\Ubuntu\\home\\me\\live' ) }) it('infers local reconstructed WSL context from a WSL UNC worktree', () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) runtime.registerPty( 'pty-reconstructed', `${TEST_REPO_ID}::\\\\wsl.localhost\\Ubuntu\\home\\me\\repo` ) runtime.onPtyData('pty-reconstructed', '\x1b]7;file://DESKTOP/home/me/repo/src\x07', 1) const cwd = ( runtime as unknown as { terminalCwdByPtyId: Map } ).terminalCwdByPtyId.get('pty-reconstructed') expect(cwd).toBe('\\\\wsl.localhost\\Ubuntu\\home\\me\\repo\\src') }) it('clears stale terminal file URI hostnames after empty-host OSC7 cwd updates', () => { const runtime = new OrcaRuntimeService(store) runtime.registerPty('pty-ssh', TEST_WORKTREE_ID, 'ssh-conn-1') runtime.onPtyData('pty-ssh', '\x1b]7;file://remote-host/home/me/repo/src\x07', 123) runtime.onPtyData('pty-ssh', '\x1b]7;file:///home/me/repo/src\x07', 124) const internals = runtime as unknown as { terminalCwdByPtyId: Map terminalFileUriHostnameByPtyId: Map } expect(internals.terminalCwdByPtyId.get('pty-ssh')).toBe('/home/me/repo/src') expect(internals.terminalFileUriHostnameByPtyId.has('pty-ssh')).toBe(false) }) it('serializes SSH headless OSC7 cwd as POSIX when the desktop runtime is on Windows', async () => { setPlatform('win32') const runtime = new OrcaRuntimeService(store) runtime.registerPty('pty-ssh', TEST_WORKTREE_ID, 'ssh-conn-1') runtime.onPtyData('pty-ssh', '\x1b]7;file://remote-host/home/me/repo/src\x07hello', 123) const snapshot = await ( runtime as unknown as { serializeHeadlessTerminalBuffer: ( ptyId: string, opts: { includeEmpty?: boolean } ) => Promise<{ cwd?: string | null } | null> } ).serializeHeadlessTerminalBuffer('pty-ssh', { includeEmpty: true }) expect(snapshot?.cwd).toBe('/home/me/repo/src') }) it('keeps Windows SSH OSC7 cwd as a drive path when the desktop runtime is POSIX', () => { setPlatform('darwin') const runtime = new OrcaRuntimeService(store) runtime.registerPty('pty-ssh-win', `${TEST_REPO_ID}::C:/Users/me/repo`, 'ssh-conn-1') runtime.onPtyData('pty-ssh-win', '\x1b]7;file:///C:/Users/me/repo/src\x07', 123) const internals = runtime as unknown as { terminalCwdByPtyId: Map } expect(internals.terminalCwdByPtyId.get('pty-ssh-win')).toBe('C:/Users/me/repo/src') }) it('serializes Windows SSH headless OSC7 cwd as a drive path on POSIX desktops', async () => { setPlatform('darwin') const runtime = new OrcaRuntimeService(store) runtime.registerPty('pty-ssh-win', `${TEST_REPO_ID}::C:/Users/me/repo`, 'ssh-conn-1') runtime.onPtyData('pty-ssh-win', '\x1b]7;file:///C:/Users/me/repo/src\x07hello', 123) const snapshot = await ( runtime as unknown as { serializeHeadlessTerminalBuffer: ( ptyId: string, opts: { includeEmpty?: boolean } ) => Promise<{ cwd?: string | null } | null> } ).serializeHeadlessTerminalBuffer('pty-ssh-win', { includeEmpty: true }) expect(snapshot?.cwd).toBe('C:/Users/me/repo/src') }) it('infers restored SSH connection identity from app-scoped PTY ids', () => { const statuses: RuntimeTerminalAgentStatusEvent[] = [] const runtime = new OrcaRuntimeService(store, undefined, { onTerminalAgentStatus: (event) => statuses.push(event) }) const ptyId = 'ssh:ssh-restored@@relay-pty' const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId } ] }) runtime.onPtyData(ptyId, '\x1b]9999;{"state":"working","agentType":"codex"}\x07', 123) expect(statuses).toEqual([ expect.objectContaining({ ptyId, connectionId: 'ssh-restored', payload: expect.objectContaining({ state: 'working', agentType: 'codex' }) }) ]) }) it('preserves OSC 9999 parser state for rendererless background PTYs', async () => { const statuses: RuntimeTerminalAgentStatusEvent[] = [] const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store, undefined, { onTerminalAgentStatus: (event) => statuses.push(event) }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', title: 'worker' }) const spawnedEnv = (spawn.mock.calls[0]?.[0] as { env?: Record } | undefined)?.env ?? {} const paneKey = expectStablePaneKeyEnv(spawnedEnv) runtime.onPtyData('pty-bg', 'before\x1b]999', 123) runtime.onPtyData('pty-bg', '9;{"state":"done","prompt":"ok"}\x1b\\after', 124) expect(statuses).toEqual([ { ptyId: 'pty-bg', source: 'pty-record', paneKey, tabId: spawnedEnv.ORCA_TAB_ID, worktreeId: TEST_WORKTREE_ID, connectionId: null, payload: { state: 'done', prompt: 'ok' } } ]) }) it('continues terminal agent status fanout when a callback throws', () => { const statuses: RuntimeTerminalAgentStatusEvent[] = [] const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const runtime = new OrcaRuntimeService(store, undefined, { onTerminalAgentStatus: (event) => { statuses.push(event) if (statuses.length === 1) { throw new Error('status listener failed') } } }) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData( 'pty-1', '\x1b]9999;{"state":"working","prompt":"one","agentType":"codex"}\x07' + '\x1b]9999;{"state":"done","prompt":"two","agentType":"codex"}\x07', 123 ) expect(statuses.map((event) => event.payload.prompt)).toEqual(['one', 'two']) expect(errorSpy).toHaveBeenCalledWith( '[runtime] terminal agent status listener threw', expect.objectContaining({ ptyId: 'pty-1', paneKey: `tab-1:${leafId}`, state: 'working', agentType: 'codex', err: expect.any(Error) }) ) }) 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('reports permission from blocked terminal wait text', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'permission' }) }) it('reports permission from blocked wait text over title-only working state', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'permission' }) }) it('lets a live non-permission title supersede stale blocked wait text', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 124) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'working' }) }) it('maps fresh explicit waiting hook state to permission over a working title', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'waiting', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'permission' }) }) it('does not let stale wait text override a fresh explicit working state', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'working' }) }) it('reports permission when blocked wait text is newer than explicit working state', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const now = Date.now() const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: now - 1000, stateStartedAt: now - 1000, tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', now) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'permission' }) }) it('timestamps blocked wait text when the prompt arrives across PTY chunks', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const now = Date.now() const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: now, stateStartedAt: now, tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'Hooks need review. ', now + 1000) runtime.onPtyData('pty-1', 'Press enter to confirm\n', now + 1001) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'permission' }) }) it('prefers newer explicit working state over older explicit permission state', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const now = Date.now() const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'waiting', prompt: '', agentType: 'codex', connectionId: null, receivedAt: now - 1000, stateStartedAt: now - 1000, tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID }, { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: now, stateStartedAt: now, tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'working' }) }) it('prefers fresh explicit working state over a stale permission title', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex - action required', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'working' }) }) it('reports permission from a live title over fresh explicit working state', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'Codex - action required' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'permission' }) }) it('does not let fresh explicit hook state authorize a current shell terminal', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'zsh', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: false, status: null }) }) it('does not let fresh explicit hook state authorize a shell foreground process', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'working', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'zsh' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: false, status: null }) }) it('uses strong provider confirmation to authorize fresh hook state over a shell foreground', async () => { const getForegroundProcess = vi.fn(async () => 'powershell.exe') const confirmForegroundProcess = vi.fn(async () => 'claude') const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess, confirmForegroundProcess }) await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({ handle, isRunningAgent: true, status: 'working' }) expect(getForegroundProcess).toHaveBeenCalledOnce() expect(getForegroundProcess).toHaveBeenCalledWith('pty-1') expect(confirmForegroundProcess).toHaveBeenCalledOnce() expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1') }) it('calls foreground confirmation with its controller receiver', async () => { const getForegroundProcess = vi.fn(async () => 'powershell.exe') const confirmForegroundProcess = vi.fn( async function (this: { getForegroundProcess: typeof getForegroundProcess }) { return this.getForegroundProcess === getForegroundProcess ? 'codex' : null } ) const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess, confirmForegroundProcess }) await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ isRunningAgent: true, status: 'working' }) expect(confirmForegroundProcess).toHaveBeenCalledOnce() }) it.each([ ['shell', async () => 'pwsh.exe'], ['non-agent', async () => 'vim'], ['unavailable', async () => null], [ 'failure', async () => { throw new Error('provider unavailable') } ] ])('fails closed when shell-conflict confirmation returns %s', async (_case, confirm) => { const confirmForegroundProcess = vi.fn(confirm) const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess: async () => 'zsh', confirmForegroundProcess }) await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({ handle, isRunningAgent: false, status: null }) expect(confirmForegroundProcess).toHaveBeenCalledOnce() }) it('fails closed on a shell conflict when the controller cannot confirm it', async () => { const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess: async () => 'zsh' }) await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({ handle, isRunningAgent: false, status: null }) }) it('skips strong confirmation when ordinary foreground evidence recognizes an agent', async () => { const getForegroundProcess = vi.fn(async () => 'codex') const confirmForegroundProcess = vi.fn(async () => 'codex') const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess, confirmForegroundProcess }) await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ isRunningAgent: true, status: 'working' }) expect(getForegroundProcess).toHaveBeenCalledOnce() expect(confirmForegroundProcess).not.toHaveBeenCalled() }) it('skips both foreground reads when current title evidence blocks explicit hook state', async () => { const getForegroundProcess = vi.fn(async () => 'zsh') const confirmForegroundProcess = vi.fn(async () => 'codex') const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess, confirmForegroundProcess, title: 'zsh' }) await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ isRunningAgent: false, status: null }) expect(getForegroundProcess).not.toHaveBeenCalled() expect(confirmForegroundProcess).not.toHaveBeenCalled() }) it('skips foreground reads for permission title and blocked wait evidence', async () => { for (const blocked of ['title', 'wait'] as const) { const getForegroundProcess = vi.fn(async () => 'zsh') const confirmForegroundProcess = vi.fn(async () => 'codex') const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess, confirmForegroundProcess }) runtime.onPtyData( 'pty-1', blocked === 'title' ? '\x1b]0;Codex waiting for permission\x07' : 'Hooks need review. Press enter to confirm\n', Date.now() + 1000 ) getForegroundProcess.mockClear() confirmForegroundProcess.mockClear() await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ isRunningAgent: true, status: 'permission' }) expect(getForegroundProcess).not.toHaveBeenCalled() expect(confirmForegroundProcess).not.toHaveBeenCalled() } }) it('rejects foreground evidence when the handle rebinds during the ordinary read', async () => { const foreground = deferred() const getForegroundProcess = vi.fn(() => foreground.promise) const confirmForegroundProcess = vi.fn(async () => 'codex') const { runtime, handle, syncPty } = await createExplicitAgentStatusHarness({ getForegroundProcess, confirmForegroundProcess }) const status = runtime.getTerminalAgentStatus(handle) await vi.waitFor(() => expect(getForegroundProcess).toHaveBeenCalledWith('pty-1')) syncPty('pty-2') foreground.resolve('zsh') await expect(status).rejects.toThrow('terminal_handle_stale') expect(confirmForegroundProcess).not.toHaveBeenCalled() }) it('rejects a handle rebind while a controller-less status check yields', async () => { const { runtime, handle, syncPty } = await createExplicitAgentStatusHarness({ getForegroundProcess: async () => 'zsh' }) runtime.setPtyController(null) const status = runtime.getTerminalAgentStatus(handle) syncPty('pty-2') await expect(status).rejects.toThrow('terminal_handle_stale') }) it('rejects confirmation evidence when the handle rebinds during the fresh read', async () => { const confirmation = deferred() const confirmForegroundProcess = vi.fn(() => confirmation.promise) const { runtime, handle, syncPty } = await createExplicitAgentStatusHarness({ getForegroundProcess: async () => 'powershell.exe', confirmForegroundProcess }) const status = runtime.getTerminalAgentStatus(handle) await vi.waitFor(() => expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1')) syncPty('pty-2') confirmation.resolve('codex') await expect(status).rejects.toThrow('terminal_handle_stale') }) it('rejects confirmation evidence when the owning PTY exits', async () => { const confirmation = deferred() const confirmForegroundProcess = vi.fn(() => confirmation.promise) const { runtime, handle } = await createExplicitAgentStatusHarness({ getForegroundProcess: async () => 'powershell.exe', confirmForegroundProcess }) const status = runtime.getTerminalAgentStatus(handle) await vi.waitFor(() => expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1')) runtime.onPtyExit('pty-1', 0) confirmation.resolve('codex') await expect(status).rejects.toThrow('terminal_exited') }) it('reports permission from a title-derived action-required agent state', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex waiting for permission', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'permission' }) }) it('maps fresh explicit done hook state to idle for send readiness', async () => { const leafId = '11111111-1111-4111-8111-111111111111' const paneKey = makePaneKey('tab-1', leafId) const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, state: 'done', prompt: '', agentType: 'codex', connectionId: null, receivedAt: Date.now(), stateStartedAt: Date.now(), tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID } ] }) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: 'idle' }) }) it('reports recognized foreground agents with unknown status as running with null status', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'repo terminal', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ handle: terminal.handle, isRunningAgent: true, status: null }) }) it('keeps ordinary terminal send suffix failures on the existing not-writable contract', async () => { const writes: string[] = [] const beforeWrite = vi.fn() const afterWrite = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: (_ptyId: string, data: string) => { writes.push(data) return data !== '\r' }, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex ready', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const [terminal] = (await runtime.listTerminals()).terminals await expect( runtime.sendTerminal( terminal.handle, { text: 'notes', enter: true }, { beforeWrite, afterWrite } ) ).rejects.toThrow('terminal_not_writable') expect(writes).toEqual(['notes', '\r']) expect(beforeWrite).toHaveBeenCalledTimes(2) expect(afterWrite).toHaveBeenCalledOnce() }) it('creates visible terminal sessions without asking the renderer to focus a tab', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const createTerminal = vi.fn() const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal, revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const result = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', launchConfig: { agentArgs: '--model gpt-5', agentEnv: { CODEX_PROFILE: 'captured' } }, title: 'worker' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: TEST_WORKTREE_PATH, command: 'codex', commandDelivery: 'provider', worktreeId: TEST_WORKTREE_ID, preAllocatedHandle: expect.stringMatching(/^term_/) }) ) expect(result).toMatchObject({ worktreeId: TEST_WORKTREE_ID, title: 'worker', surface: 'visible' }) expect(result.handle).toMatch(/^term_/) expect(createTerminal).not.toHaveBeenCalled() // Why: agent status keys off `${tabId}:${leafId}`; main pre-allocates the tabId, env-stamps it before spawn, and reuses it for adoption. const spawnCall = spawn.mock.calls[0]?.[0] as { env?: Record } | undefined const spawnedEnv = spawnCall?.env ?? {} expectStablePaneKeyEnv(spawnedEnv) const spawnedLeafId = spawnedEnv.ORCA_PANE_KEY.slice(`${spawnedEnv.ORCA_TAB_ID}:`.length) expect(spawnedEnv.ORCA_WORKTREE_ID).toBe(TEST_WORKTREE_ID) expect(spawnedEnv.ORCA_AGENT_LAUNCH_TOKEN).toMatch(UUID_RE) expect(revealTerminalSession).toHaveBeenCalledWith(TEST_WORKTREE_ID, { ptyId: 'pty-bg', title: 'worker', launchConfig: { agentArgs: '--model gpt-5', agentEnv: { CODEX_PROFILE: 'captured' } }, launchToken: spawnedEnv.ORCA_AGENT_LAUNCH_TOKEN, activate: false, tabId: spawnedEnv.ORCA_TAB_ID, leafId: spawnedLeafId }) }) it('passes cached view colors to background agent spawns for source-owned startup replies', async () => { setTerminalViewAttributes({ foreground: [0xff, 0xff, 0xff], background: [0x28, 0x2c, 0x34], cursor: [0xff, 0xff, 0xff], ansi: Array.from({ length: 256 }, () => [0, 0, 0] as [number, number, number]), colorSchemeMode: 'dark', cursorStyle: 'block', cursorBlink: false }) const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ terminalColorQueryReplies: { foreground: '#ffffff', background: '#282c34' } }) ) }) it('does not register or publish a PTY incarnation that exited before spawn resolved', async () => { const runtime = new OrcaRuntimeService(store) const tabId = '11111111-1111-4111-8111-111111111111' const leafId = '22222222-2222-4222-8222-222222222222' runtime.setPtyController({ spawn: vi.fn(async () => { runtime.beginPtyRegistration('pty-exited-during-start', 'incarnation-exited-during-start') runtime.onPtyExit('pty-exited-during-start', 0, 'incarnation-exited-during-start') return { id: 'pty-exited-during-start', incarnationId: 'incarnation-exited-during-start' } }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) await expect( runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', presentation: 'background', tabId, leafId, persistHostSessionBinding: true }) ).rejects.toThrow('agent_session_exited_during_start') await expect(runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)).resolves.toMatchObject({ terminals: [] }) await expect(runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).resolves.toMatchObject({ tabs: [] }) const internals = runtime as unknown as { handleByPtyId: Map ptysById: Map } expect(internals.handleByPtyId.has('pty-exited-during-start')).toBe(false) expect(internals.ptysById.has('pty-exited-during-start')).toBe(false) }) it('adopts the execution owner canonical surface for repeated structured resumes', async () => { let canonicalOwner: | { claim: AgentSessionExecutionClaim generation: string phase: 'live' ptyId: string surface: AgentSessionSurfaceBinding } | undefined const spawn = vi.fn(async (options) => { const ensure = options.agentSessionEnsure expect(ensure).toBeDefined() canonicalOwner ??= { claim: ensure!.claim, generation: 'generation-1', phase: 'live', ptyId: 'pty-claimed', surface: ensure!.surface } return { id: 'pty-claimed', agentSessionEnsure: { disposition: spawn.mock.calls.length === 1 ? ('created' as const) : ('adopted' as const), owner: canonicalOwner } } }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const request = { kind: 'explicit' as const, worktree: `id:${TEST_WORKTREE_ID}`, agent: 'codex' as const, providerSession: { key: 'session_id' as const, id: 'provider-session-1' } } const first = await runtime.ensureAgentSession(request) const second = await runtime.ensureAgentSession(request) expect(first.disposition).toBe('created') expect(second.disposition).toBe('adopted') expect(second.terminal).toMatchObject({ handle: first.terminal.handle, tabId: first.terminal.tabId, paneKey: first.terminal.paneKey }) expect(spawn).toHaveBeenCalledTimes(2) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: expect.stringContaining("'resume' 'provider-session-1'"), agentSessionEnsure: expect.objectContaining({ claim: expect.objectContaining({ agent: 'codex' }) }) }) ) }) it('builds structured fresh drafts with supported launch preferences on the host', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent-draft' }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: { claude: 'host-claude' }, agentDefaultArgs: { claude: '--host-default' }, agentDefaultEnv: { claude: { HOST_PROFILE: 'true' } } }) }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createAgentSession( { clientOperationId: `${Date.now()}-${'ab'.repeat(16)}`, worktree: `id:${TEST_WORKTREE_ID}`, agent: 'claude', prompt: 'review before sending', promptDelivery: 'draft', agentArgs: '--permission-mode plan', launchPreferences: { model: 'opus', effort: 'high' } }, { clientId: 'renderer-1', clientKind: 'runtime' } ) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: expect.stringMatching( /^host-claude '--model' 'opus'.*'--permission-mode' 'plan'.*--prefill 'review before sending'/ ), env: expect.objectContaining({ HOST_PROFILE: 'true' }) }) ) expect(spawn.mock.calls[0]?.[0]?.command).not.toContain('--host-default') }) it('applies Settings agent defaults to bare agent command terminal creates', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: {}, agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, agentDefaultEnv: { codex: { CODEX_PROFILE: 'captured' } } }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', title: 'worker' }) const spawnCall = spawn.mock.calls[0]?.[0] as | { command?: string; env?: Record } | undefined expect(spawnCall?.command).toBe("codex '--dangerously-bypass-approvals-and-sandbox'") expect(spawnCall?.env).toMatchObject({ CODEX_PROFILE: 'captured', ORCA_WORKTREE_ID: TEST_WORKTREE_ID }) expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toMatch(UUID_RE) expect(markCodexProjectTrustedMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH) expect(markCodexProjectTrustedMock.mock.invocationCallOrder[0]).toBeLessThan( spawn.mock.invocationCallOrder[0]! ) }) it('quotes local Windows bare agent command defaults for cmd.exe terminal creates', async () => { setPlatform('win32') const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], terminalWindowsShell: 'cmd.exe', agentCmdOverrides: {}, agentDefaultArgs: { claude: '--dangerously-skip-permissions' }, agentDefaultEnv: {} }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude', title: 'worker' }) const spawnCall = spawn.mock.calls[0]?.[0] as { command?: string } | undefined expect(spawnCall?.command).toBe('claude "--dangerously-skip-permissions"') }) it('does not use the local Windows shell setting for remote Windows bare agent creates', async () => { const remoteRepo = { id: TEST_REPO_ID, path: 'C:/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const remoteStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], terminalWindowsShell: 'cmd.exe', agentCmdOverrides: {}, agentDefaultArgs: { claude: '--dangerously-skip-permissions' }, agentDefaultEnv: {} }) } const provider = { exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }), listWorktrees: vi.fn().mockResolvedValue([ { path: 'C:/remote/repo', head: 'abc', branch: 'main', isBare: false, isMainWorktree: true } ]) } const spawn = vi.fn().mockResolvedValue({ id: 'pty-remote-windows-bare' }) registerSshGitProvider('ssh-1', provider as never) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) try { const terminal = await runtime.createTerminal('path:C:/remote/repo', { command: 'claude', title: 'worker' }) const spawnCall = spawn.mock.calls[0]?.[0] as { command?: string } | undefined expect(spawnCall?.command).toBe("claude '--dangerously-skip-permissions'") expect(terminal).toMatchObject({ executionHostId: 'ssh:ssh-1', hostPlatform: 'linux' }) } finally { unregisterSshGitProvider('ssh-1') } }) it('matches canonical bare agent commands when a command override is configured', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: { codex: 'codex --profile work' }, agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, agentDefaultEnv: {} }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex' }) const spawnCall = spawn.mock.calls[0]?.[0] as { command?: string } | undefined expect(spawnCall?.command).toBe( "codex --profile work '--dangerously-bypass-approvals-and-sandbox'" ) }) it('keeps non-bare agent command terminal creates unchanged', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: {}, agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, agentDefaultEnv: {} }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex exec summarize' }) const spawnCall = spawn.mock.calls[0]?.[0] as | { command?: string; env?: Record } | undefined expect(spawnCall?.command).toBe('codex exec summarize') expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toBeUndefined() }) it('keeps disabled bare agent command terminal creates unchanged', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: ['codex' as const], agentCmdOverrides: {}, agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, agentDefaultEnv: {} }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex' }) const spawnCall = spawn.mock.calls[0]?.[0] as | { command?: string; env?: Record } | undefined expect(spawnCall?.command).toBe('codex') expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toBeUndefined() }) it('sends Settings agent defaults through renderer-backed bare agent terminal creates', async () => { const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: {}, agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, agentDefaultEnv: { codex: { CODEX_PROFILE: 'captured' } } }) } const runtime = new OrcaRuntimeService(runtimeStore) const webContents = { send: vi.fn() } webContents.send.mockImplementation((_channel: string, payload: { requestId: string }) => { runtime.syncWindowGraph(1, { tabs: [], leaves: [ { tabId: 'tab-renderer', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-renderer', paneTitle: null } ] }) ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-renderer', title: 'Codex' } ) }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', rendererBacked: true }) expect(webContents.send).toHaveBeenCalledWith( 'terminal:requestTabCreate', expect.objectContaining({ worktreeId: TEST_WORKTREE_ID, command: "codex '--dangerously-bypass-approvals-and-sandbox'", env: { CODEX_PROFILE: 'captured' }, launchAgent: 'codex', launchConfig: { agentCommand: "codex '--dangerously-bypass-approvals-and-sandbox'", agentArgs: '--dangerously-bypass-approvals-and-sandbox', agentEnv: { CODEX_PROFILE: 'captured' } } }) ) expect(markCodexProjectTrustedMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH) expect(markCodexProjectTrustedMock.mock.invocationCallOrder[0]).toBeLessThan( webContents.send.mock.invocationCallOrder[0]! ) }) it('injects runtime hook receiver env into terminal sessions', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-hooked' }) const runtime = new OrcaRuntimeService(store, undefined, { buildAgentHookPtyEnv: () => ({ ORCA_AGENT_HOOK_PORT: '5678', ORCA_AGENT_HOOK_TOKEN: 'agent-token', ORCA_AGENT_HOOK_ENV: 'remote', ORCA_AGENT_HOOK_VERSION: '1' }) }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', env: { ORCA_AGENT_HOOK_PORT: '1111', ORCA_AGENT_HOOK_TOKEN: 'stale-token', ORCA_AGENT_HOOK_ENDPOINT: '/tmp/stale-endpoint.env' }, envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'] }) const spawnCall = spawn.mock.calls[0]?.[0] as | { env?: Record; envToDelete?: string[] } | undefined expect(spawnCall?.env).toEqual( expect.objectContaining({ ORCA_AGENT_HOOK_PORT: '5678', ORCA_AGENT_HOOK_TOKEN: 'agent-token', ORCA_AGENT_HOOK_ENV: 'remote', ORCA_AGENT_HOOK_VERSION: '1', ORCA_PANE_KEY: expect.any(String), ORCA_TAB_ID: expect.any(String), ORCA_WORKTREE_ID: TEST_WORKTREE_ID }) ) expect(spawnCall?.env?.ORCA_AGENT_HOOK_ENDPOINT).toBeUndefined() expect(spawnCall?.envToDelete).toEqual(['CODEX_HOME', 'ORCA_CODEX_HOME']) }) it.each([ { label: 'canonical folder workspace selector', selector: TEST_FOLDER_WORKSPACE_KEY }, { label: 'id-prefixed folder workspace selector', selector: `id:${TEST_FOLDER_WORKSPACE_KEY}` } ])('creates background terminal sessions for a $label', async ({ selector }) => { const folderPath = await mkdtemp(join(tmpdir(), 'orca-runtime-folder-workspace-')) const spawn = vi.fn().mockResolvedValue({ id: 'pty-folder' }) const folderWorkspace = makeFolderWorkspace({ folderPath }) const projectGroup = makeFolderProjectGroup({ parentPath: folderPath }) const runtime = new OrcaRuntimeService( createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never ) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await expect( runtime.createTerminal(selector, { command: 'codex', title: 'multi-repo worker' }) ).resolves.toMatchObject({ worktreeId: TEST_FOLDER_WORKSPACE_KEY, title: 'multi-repo worker', surface: 'background' }) const spawnCall = spawn.mock.calls[0]?.[0] as | { cwd?: string; env?: Record; worktreeId?: string } | undefined const spawnedEnv = spawnCall?.env ?? {} expect(spawnCall).toMatchObject({ cwd: folderPath, worktreeId: TEST_FOLDER_WORKSPACE_KEY }) expectStablePaneKeyEnv(spawnedEnv) expect(spawnedEnv.ORCA_WORKSPACE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY) expect(spawnedEnv.ORCA_PROJECT_GROUP_ID).toBe(TEST_FOLDER_PROJECT_GROUP_ID) expect(spawnedEnv.ORCA_WORKSPACE_ROOT).toBe(folderPath) expect(spawnedEnv.ORCA_WORKTREE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY) }) it.each([ { label: 'bare floating terminal sentinel', selector: FLOATING_TERMINAL_WORKTREE_ID }, { label: 'id-prefixed floating terminal sentinel', selector: `id:${FLOATING_TERMINAL_WORKTREE_ID}` } ])('creates background terminal sessions for a $label', async ({ selector }) => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-floating' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await expect( runtime.createTerminal(selector, { command: 'codex', title: 'floating worker' }) ).resolves.toMatchObject({ worktreeId: FLOATING_TERMINAL_WORKTREE_ID, title: 'floating worker', surface: 'background' }) const spawnCall = spawn.mock.calls[0]?.[0] as | { cwd?: string connectionId?: string | null env?: Record worktreeId?: string } | undefined expect(spawnCall).toMatchObject({ cwd: homedir(), connectionId: null, worktreeId: FLOATING_TERMINAL_WORKTREE_ID }) expect(spawnCall?.env?.ORCA_WORKTREE_ID).toBe(FLOATING_TERMINAL_WORKTREE_ID) expect(spawnCall?.env?.ORCA_WORKSPACE_ID).toBeUndefined() expect(spawnCall?.env?.ORCA_PROJECT_GROUP_ID).toBeUndefined() expect(spawnCall?.env?.ORCA_WORKSPACE_ROOT).toBeUndefined() }) it('rejects folder workspace terminal creation when the backing path is missing', async () => { const missingPath = join(tmpdir(), `orca-missing-folder-workspace-${randomUUID()}`) const spawn = vi.fn().mockResolvedValue({ id: 'pty-folder' }) const folderWorkspace = makeFolderWorkspace({ folderPath: missingPath }) const projectGroup = makeFolderProjectGroup({ parentPath: missingPath }) const runtime = new OrcaRuntimeService( createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never ) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await expect(runtime.createTerminal(TEST_FOLDER_WORKSPACE_KEY)).rejects.toThrow( 'folder_workspace_path_missing' ) expect(spawn).not.toHaveBeenCalled() }) it('rejects folder workspace folderPath updates when the new path is missing', async () => { const missingPath = join(tmpdir(), `orca-missing-folder-update-${randomUUID()}`) const folderWorkspace = makeFolderWorkspace() const runtimeStore = { ...createFolderWorkspaceRuntimeStore(folderWorkspace), updateFolderWorkspace: vi.fn() } const runtime = new OrcaRuntimeService(runtimeStore as never) await expect( runtime.updateFolderWorkspace(TEST_FOLDER_WORKSPACE_ID, { folderPath: missingPath }) ).rejects.toThrow('folder_workspace_path_missing') expect(runtimeStore.updateFolderWorkspace).not.toHaveBeenCalled() }) it('enables Claude Agent Teams only for direct Claude launches when configured in-process', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), claudeAgentTeamsMode: 'in-process' as const }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: "claude 'hello'" }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: "echo ok; claude 'hello'" }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex' }) const directClaude = spawn.mock.calls[0]?.[0] as { command?: string env?: Record } const compoundClaude = spawn.mock.calls[1]?.[0] as { command?: string env?: Record } const normalAgent = spawn.mock.calls[2]?.[0] as { command?: string env?: Record } expect(directClaude.command).toBe("claude --teammate-mode in-process 'hello'") expect(directClaude.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBe('1') expect(directClaude.env?.TMUX).toBeUndefined() expect(compoundClaude.command).toBe("echo ok; claude 'hello'") expect(compoundClaude.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBeUndefined() expect(compoundClaude.env?.TMUX).toBeUndefined() expect(normalAgent.command).toBe("codex '--dangerously-bypass-approvals-and-sandbox'") expect(normalAgent.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBeUndefined() expect(normalAgent.env?.TMUX).toBeUndefined() }) it('reveals Claude Agent Teams launches with the rewritten launch config', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), claudeAgentTeamsMode: 'in-process' as const }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: "claude 'hello'", launchAgent: 'claude', launchConfig: { agentCommand: 'claude', agentArgs: '', agentEnv: { CLAUDE_PROFILE: 'captured' } } }) expect(spawn).toHaveBeenCalledWith(expect.objectContaining({ launchAgent: 'claude' })) const spawnCall = spawn.mock.calls[0]?.[0] as { env?: Record } | undefined const spawnedEnv = spawnCall?.env ?? {} const spawnedLeafId = spawnedEnv.ORCA_PANE_KEY.slice(`${spawnedEnv.ORCA_TAB_ID}:`.length) expect(revealTerminalSession).toHaveBeenCalledWith(TEST_WORKTREE_ID, { ptyId: 'pty-bg', title: null, launchConfig: { agentCommand: 'claude --teammate-mode in-process', agentArgs: '', agentEnv: { CLAUDE_PROFILE: 'captured', CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' } }, launchToken: spawnedEnv.ORCA_AGENT_LAUNCH_TOKEN, launchAgent: 'claude', activate: false, tabId: spawnedEnv.ORCA_TAB_ID, leafId: spawnedLeafId }) }) it('preserves Claude Agent Teams for sequenced Claude launches', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), claudeAgentTeamsMode: 'in-process' as const }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'bash -lc \'echo Waiting for setup to finish before starting agent... >&2; exec claude "hello"\'', claudeAgentTeamsSourceCommand: 'claude "hello"', launchAgent: 'claude', launchConfig: { agentCommand: 'claude', agentArgs: '', agentEnv: { CLAUDE_PROFILE: 'captured' } } }) const sequencedClaude = spawn.mock.calls[0]?.[0] as { command?: string env?: Record } expect(sequencedClaude.command).toBe( 'bash -lc \'echo Waiting for setup to finish before starting agent... >&2; exec claude "hello"\'' ) expect(sequencedClaude.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBe('1') expect(sequencedClaude.env?.[SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]).toBe( 'claude --teammate-mode in-process "hello"' ) }) it('restores captured native Claude Agent Teams mode with fresh service env', async () => { setPlatform('linux') const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), claudeAgentTeamsMode: 'off' as const }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude --resume claude-session', env: { CLAUDE_PROFILE: 'captured', ORCA_AGENT_TEAMS_TEAM_ID: 'stale-team', ORCA_AGENT_TEAMS_TOKEN: 'stale-token', TMUX: '/tmp/orca-claude-agent-teams/stale-team,0,1' }, launchAgent: 'claude', launchConfig: { agentCommand: 'claude', agentArgs: '--teammate-mode auto', agentEnv: { CLAUDE_PROFILE: 'captured', ORCA_AGENT_TEAMS_TEAM_ID: 'stale-team', ORCA_AGENT_TEAMS_TOKEN: 'stale-token', TMUX: '/tmp/orca-claude-agent-teams/stale-team,0,1' } } }) const spawnCall = spawn.mock.calls[0]?.[0] as | { command?: string; env?: Record } | undefined expect(spawnCall?.command).toBe('claude --teammate-mode auto --resume claude-session') expect(spawnCall?.env).toMatchObject({ CLAUDE_PROFILE: 'captured', CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', TMUX_PANE: '%1' }) expect(spawnCall?.env?.ORCA_AGENT_TEAMS_TEAM_ID).toMatch(/^team-/) expect(spawnCall?.env?.ORCA_AGENT_TEAMS_TEAM_ID).not.toBe('stale-team') expect(spawnCall?.env?.ORCA_AGENT_TEAMS_TOKEN).not.toBe('stale-token') expect(spawnCall?.env?.TMUX).not.toBe('/tmp/orca-claude-agent-teams/stale-team,0,1') expect(revealTerminalSession).toHaveBeenCalledWith( TEST_WORKTREE_ID, expect.objectContaining({ launchConfig: expect.objectContaining({ agentCommand: 'claude --teammate-mode auto', agentEnv: expect.objectContaining({ CLAUDE_PROFILE: 'captured', CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', TMUX_PANE: '%1' }) }), launchAgent: 'claude' }) ) const revealedLaunchConfig = revealTerminalSession.mock.calls[0]?.[1]?.launchConfig expect(revealedLaunchConfig?.agentEnv.ORCA_AGENT_TEAMS_TEAM_ID).not.toBe('stale-team') expect(revealedLaunchConfig?.agentEnv.ORCA_AGENT_TEAMS_TOKEN).not.toBe('stale-token') }) it('does not apply current Agent Teams mode to captured plain Claude resumes', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg' }) const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), claudeAgentTeamsMode: 'in-process' as const }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude --resume claude-session', launchAgent: 'claude', launchConfig: { agentCommand: 'claude', agentArgs: '', agentEnv: { CLAUDE_PROFILE: 'captured' } } }) const spawnCall = spawn.mock.calls[0]?.[0] as | { command?: string; env?: Record } | undefined expect(spawnCall?.command).toBe('claude --resume claude-session') expect(spawnCall?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBeUndefined() expect(revealTerminalSession).toHaveBeenCalledWith( TEST_WORKTREE_ID, expect.objectContaining({ launchConfig: { agentCommand: 'claude', agentArgs: '', agentEnv: { CLAUDE_PROFILE: 'captured' } }, launchAgent: 'claude' }) ) }) it('adopts renderer pane identity for remote runtime terminal creates', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) const tabId = 'tab-remote-runtime' const leafId = '11111111-1111-4111-8111-111111111111' runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { focus: false, tabId, leafId, env: { ORCA_PANE_KEY: `${tabId}:${leafId}`, ORCA_TAB_ID: tabId } }) const spawnedEnv = (spawn.mock.calls[0]?.[0] as { env?: Record } | undefined)?.env ?? {} expect(spawnedEnv.ORCA_TAB_ID).toBe(tabId) expect(spawnedEnv.ORCA_PANE_KEY).toBe(`${tabId}:${leafId}`) }) it('does not adopt web mirror ids as host terminal ids', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) const tabId = 'web-terminal-host-tab-1' const leafId = '11111111-1111-4111-8111-111111111111' runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { focus: false, tabId, leafId }) const spawnedEnv = (spawn.mock.calls[0]?.[0] as { env?: Record } | undefined)?.env ?? {} expect(spawnedEnv.ORCA_TAB_ID).not.toBe(tabId) expect(spawnedEnv.ORCA_TAB_ID).not.toMatch(/^web-terminal-/) expect(spawnedEnv.ORCA_PANE_KEY).toMatch(`${spawnedEnv.ORCA_TAB_ID}:`) }) it('creates background terminal sessions while the renderer graph is unavailable', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await expect(runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`)).resolves.toMatchObject({ worktreeId: TEST_WORKTREE_ID, surface: 'background' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ worktreeId: TEST_WORKTREE_ID, persistHostSessionBinding: true }) ) }) it('keeps ordinary desktop background terminal persistence opt-in', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) const webContents = { send: vi.fn() } electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents } as never) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const spawnOptions = spawn.mock.calls[0]?.[0] as | { persistHostSessionBinding?: boolean } | undefined expect(spawnOptions?.persistHostSessionBinding).toBeUndefined() }) it('falls back to background terminal creation for renderer-backed requests without a renderer window', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await expect( runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', rendererBacked: true }) ).resolves.toMatchObject({ worktreeId: TEST_WORKTREE_ID, surface: 'background' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: "codex '--dangerously-bypass-approvals-and-sandbox'", cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID }) ) }) it('accepts renderer-backed terminal create replies only from the target renderer', async () => { const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: { send: vi.fn() } }, { requestId: payload.requestId, error: 'spoofed renderer reply' } ) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-renderer', worktreeId: TEST_WORKTREE_ID, title: 'Renderer Terminal', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-renderer', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-renderer', paneTitle: null } ] }) ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-renderer', title: 'Renderer Terminal' } ) }) webContents.send = send const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) await expect( runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { command: 'codex', rendererBacked: true, title: 'Renderer Terminal' }) ).resolves.toMatchObject({ handle: expect.stringMatching(/^term_/), tabId: 'tab-renderer', title: 'Renderer Terminal', worktreeId: TEST_WORKTREE_ID, surface: 'visible' }) expect(send).toHaveBeenCalledWith( 'terminal:requestTabCreate', expect.objectContaining({ requestId: expect.any(String), worktreeId: TEST_WORKTREE_ID, command: "codex '--dangerously-bypass-approvals-and-sandbox'", title: 'Renderer Terminal' }) ) expect(electronMocks.ipcMain.removeListener).toHaveBeenCalledWith( 'terminal:tabCreateReply', expect.any(Function) ) }) it('splits visible pty-backed terminal sessions through the parent renderer tab', async () => { const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-source' }) .mockResolvedValueOnce({ id: 'pty-split' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg' }) const splitTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal, renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const sourceEnv = (spawn.mock.calls[0]?.[0] as { env?: Record } | undefined)?.env ?? {} const sourceLeafId = sourceEnv.ORCA_PANE_KEY.slice(`${sourceEnv.ORCA_TAB_ID}:`.length) await expect(runtime.splitTerminal(handle, { direction: 'vertical' })).resolves.toMatchObject({ handle: expect.stringMatching(/^term_/), tabId: sourceEnv.ORCA_TAB_ID, paneRuntimeId: -1 }) const splitEnv = (spawn.mock.calls[1]?.[0] as { env?: Record } | undefined)?.env ?? {} const splitLeafId = splitEnv.ORCA_PANE_KEY.slice(`${sourceEnv.ORCA_TAB_ID}:`.length) expect(splitTerminal).not.toHaveBeenCalled() expect(splitEnv.ORCA_TAB_ID).toBe(sourceEnv.ORCA_TAB_ID) expect(splitEnv.ORCA_WORKTREE_ID).toBe(TEST_WORKTREE_ID) expect(revealTerminalSession).toHaveBeenLastCalledWith(TEST_WORKTREE_ID, { ptyId: 'pty-split', title: null, activate: true, tabId: sourceEnv.ORCA_TAB_ID, leafId: splitLeafId, splitFromLeafId: sourceLeafId, splitDirection: 'vertical' }) // Why: client renders the tab from one sibling's parentLayout, so all siblings must carry the direction or Split Right flips down. const publishedTabs = runtime['mobileSessionTabsByWorktree'].get(TEST_WORKTREE_ID)!.tabs const siblingSurfaces = publishedTabs.filter( (tab): tab is Extract => tab.type === 'terminal' && tab.parentTabId === sourceEnv.ORCA_TAB_ID ) expect(siblingSurfaces.length).toBe(2) for (const surface of siblingSurfaces) { expect(surface.parentLayout?.root).toMatchObject({ type: 'split', direction: 'vertical' }) } }) it('splits folder workspace pty-backed terminal sessions with folder cwd and env', async () => { const folderPath = await mkdtemp(join(tmpdir(), 'orca-runtime-folder-split-')) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-folder-source' }) .mockResolvedValueOnce({ id: 'pty-folder-split' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-folder' }) const folderWorkspace = makeFolderWorkspace({ folderPath }) const projectGroup = makeFolderProjectGroup({ parentPath: folderPath }) const runtime = new OrcaRuntimeService( createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never ) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(TEST_FOLDER_WORKSPACE_KEY) const sourceCall = spawn.mock.calls[0]?.[0] as | { cwd?: string; env?: Record; worktreeId?: string } | undefined const sourceEnv = sourceCall?.env ?? {} const sourceLeafId = sourceEnv.ORCA_PANE_KEY.slice(`${sourceEnv.ORCA_TAB_ID}:`.length) await expect(runtime.splitTerminal(handle, { direction: 'vertical' })).resolves.toMatchObject({ handle: expect.stringMatching(/^term_/), tabId: sourceEnv.ORCA_TAB_ID, paneRuntimeId: -1 }) const splitCall = spawn.mock.calls[1]?.[0] as | { cwd?: string; env?: Record; worktreeId?: string } | undefined const splitEnv = splitCall?.env ?? {} const splitLeafId = splitEnv.ORCA_PANE_KEY.slice(`${sourceEnv.ORCA_TAB_ID}:`.length) expect(sourceCall).toMatchObject({ cwd: folderPath, worktreeId: TEST_FOLDER_WORKSPACE_KEY }) expect(splitCall).toMatchObject({ cwd: folderPath, worktreeId: TEST_FOLDER_WORKSPACE_KEY }) expectStablePaneKeyEnv(splitEnv) expect(splitEnv.ORCA_TAB_ID).toBe(sourceEnv.ORCA_TAB_ID) expect(splitEnv.ORCA_WORKSPACE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY) expect(splitEnv.ORCA_PROJECT_GROUP_ID).toBe(TEST_FOLDER_PROJECT_GROUP_ID) expect(splitEnv.ORCA_WORKSPACE_ROOT).toBe(folderPath) expect(splitEnv.ORCA_WORKTREE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY) expect(revealTerminalSession).toHaveBeenLastCalledWith(TEST_FOLDER_WORKSPACE_KEY, { ptyId: 'pty-folder-split', title: null, activate: true, tabId: sourceEnv.ORCA_TAB_ID, leafId: splitLeafId, splitFromLeafId: sourceLeafId, splitDirection: 'vertical' }) }) it('returns an actionable discoverability warning when default adoption fails after spawn', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const revealTerminalSession = vi.fn().mockRejectedValue(new Error('Renderer timed out')) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) try { const created = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) expect(created).toMatchObject({ worktreeId: TEST_WORKTREE_ID, surface: 'background', handle: expect.stringMatching(/^term_/) }) expect(created.warning).toContain('Renderer timed out') expect(created.warning).toContain('could not make it discoverable') expect(created.warning).toContain(`orca terminal focus --terminal ${created.handle}`) const spawnCall = spawn.mock.calls[0]?.[0] as { env?: Record } | undefined const spawnedEnv = spawnCall?.env ?? {} expectStablePaneKeyEnv(spawnedEnv) const spawnedLeafId = spawnedEnv.ORCA_PANE_KEY.slice(`${spawnedEnv.ORCA_TAB_ID}:`.length) expect(revealTerminalSession).toHaveBeenCalledWith(TEST_WORKTREE_ID, { ptyId: 'pty-bg', title: null, activate: false, tabId: spawnedEnv.ORCA_TAB_ID, leafId: spawnedLeafId }) expect(warn).toHaveBeenCalledWith( expect.stringContaining('[terminal-create] failed to create inactive tab for pty-bg:'), expect.any(Error) ) } finally { warn.mockRestore() } }) it('returns an actionable warning when default discoverability has no renderer notifier', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const created = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) expect(created).toMatchObject({ worktreeId: TEST_WORKTREE_ID, surface: 'background', handle: expect.stringMatching(/^term_/) }) expect(created.warning).toContain('could not make it discoverable') expect(created.warning).toContain(`orca terminal focus --terminal ${created.handle}`) }) it('does not warn when background presentation has no renderer notifier', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const created = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { presentation: 'background' }) expect(created).toMatchObject({ worktreeId: TEST_WORKTREE_ID, surface: 'background', handle: expect.stringMatching(/^term_/) }) expect(created.warning).toBeUndefined() }) it('waits for exit on background terminal handles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const waiting = runtime.waitForTerminal(handle, { condition: 'exit', timeoutMs: 1000 }) runtime.onPtyExit('pty-bg', 7) await expect(waiting).resolves.toMatchObject({ handle, condition: 'exit', status: 'exited', exitCode: 7 }) await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ status: 'exited' }) }) it('drops retained PTY transcript memory when a background terminal exits', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', `${Array.from({ length: 20 }, (_, i) => `line-${i}`).join('\n')}\nwrote /tmp/exited-result.json\n`, 100 ) await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ status: 'running', tail: expect.arrayContaining(['line-0']) }) runtime.onPtyExit('pty-bg', 0) const pty = ( runtime as unknown as { ptysById: Map< string, { tailBuffer: string[] tailPartialLine: string tailLinesTotal: number tailTruncated: boolean } > recentPtyPathCandidatesById: Map } ).ptysById.get('pty-bg') expect(pty).toMatchObject({ tailBuffer: [], tailPartialLine: '', tailLinesTotal: 0, tailTruncated: false }) await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ status: 'exited', tail: [] }) expect( ( runtime as unknown as { recentPtyPathCandidatesById: Map } ).recentPtyPathCandidatesById.has('pty-bg') ).toBe(false) }) it('bounds disconnected background PTY records and their synthetic handles', async () => { let nextPtyIndex = 0 const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockImplementation(async () => ({ id: `pty-bg-${nextPtyIndex++}` })), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const handles: string[] = [] for (let index = 0; index < 140; index += 1) { const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) handles.push(handle) runtime.onPtyData(`pty-bg-${index}`, `wrote /tmp/result-${index}.json\n`, 100 + index) runtime.onPtyExit(`pty-bg-${index}`, 0) } const internals = runtime as unknown as { ptysById: Map handles: Map handleByPtyId: Map recentPtyPathCandidatesById: Map } expect(internals.ptysById.size).toBeLessThanOrEqual(128) expect(internals.ptysById.has('pty-bg-0')).toBe(false) expect(internals.ptysById.has('pty-bg-139')).toBe(true) expect(internals.handleByPtyId.has('pty-bg-0')).toBe(false) expect(internals.handles.has(handles[0]!)).toBe(false) expect(internals.recentPtyPathCandidatesById.has('pty-bg-0')).toBe(false) await expect(runtime.readTerminal(handles[0]!)).rejects.toThrow('terminal_handle_stale') await expect(runtime.readTerminal(handles.at(-1)!)).resolves.toMatchObject({ status: 'exited' }) }) it('keeps retained PTY transcript memory when controller refresh omits a record', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) runtime.registerPty('daemon-pty-1', TEST_WORKTREE_ID) runtime.onPtyData('daemon-pty-1', 'still live\npartial', 100) await runtime.listTerminals() const pty = ( runtime as unknown as { ptysById: Map< string, { connected: boolean tailBuffer: string[] tailPartialLine: string tailLinesTotal: number } > } ).ptysById.get('daemon-pty-1') expect(pty).toMatchObject({ connected: false, tailBuffer: ['still live'], tailPartialLine: 'partial', tailLinesTotal: 1 }) }) it('keeps retained PTY transcript memory when controller refresh fails', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => { throw new Error('controller unavailable') } }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) runtime.registerPty('daemon-pty-1', TEST_WORKTREE_ID) runtime.onPtyData('daemon-pty-1', 'still live\npartial', 100) await runtime.listTerminals() const pty = ( runtime as unknown as { ptysById: Map< string, { connected: boolean tailBuffer: string[] tailPartialLine: string tailLinesTotal: number } > } ).ptysById.get('daemon-pty-1') expect(pty).toMatchObject({ connected: true, tailBuffer: ['still live'], tailPartialLine: 'partial', tailLinesTotal: 1 }) }) it('keeps retained PTY transcript memory when controller refresh times out', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: () => new Promise(() => {}) }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) runtime.registerPty('daemon-pty-1', TEST_WORKTREE_ID) runtime.onPtyData('daemon-pty-1', 'still live\npartial', 100) const terminals = runtime.listTerminals() await vi.advanceTimersByTimeAsync(3_000) await terminals const pty = ( runtime as unknown as { ptysById: Map< string, { connected: boolean tailBuffer: string[] tailPartialLine: string tailLinesTotal: number } > } ).ptysById.get('daemon-pty-1') expect(pty).toMatchObject({ connected: true, tailBuffer: ['still live'], tailPartialLine: 'partial', tailLinesTotal: 1 }) } finally { vi.useRealTimers() } }) it('resolves tui-idle for adopted background PTY handles from the renderer title', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-bg', worktreeId: TEST_WORKTREE_ID, title: 'Codex ready', activeLeafId: 'pane-bg', layout: null } ], leaves: [ { tabId: 'tab-bg', worktreeId: TEST_WORKTREE_ID, leafId: 'pane-bg', paneRuntimeId: 1, ptyId: 'pty-bg', paneTitle: null } ] }) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', status: 'running' }) }) it('does not treat a Codex launch title as tui-idle readiness', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-bg', worktreeId: TEST_WORKTREE_ID, title: 'Codex YOLO', activeLeafId: 'pane-bg', layout: null } ], leaves: [ { tabId: 'tab-bg', worktreeId: TEST_WORKTREE_ID, leafId: 'pane-bg', paneRuntimeId: 1, ptyId: 'pty-bg', paneTitle: null } ] }) const waitPromise = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) const timeoutAssertion = expect(waitPromise).rejects.toThrow('timeout') await vi.advanceTimersByTimeAsync(2_000) await timeoutAssertion } finally { vi.useRealTimers() } }) it('resolves tui-idle from a Codex ready prompt preview', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ ' >_ OpenAI Codex (v0.131.0)\n', ' model: gpt-5.5 high /model to change\n', ' directory: ~/orca/workspaces/orca/cli-debug\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', status: 'running' }) }) it('resolves tui-idle from an Antigravity ready prompt preview', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData('pty-bg', antigravityReadyScreen('Gemini 4 Experimental (High)'), Date.now()) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', status: 'running' }) }) it('resolves Antigravity ready prompts with newline-heavy pasted tails without splitting', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) let pastedTail = '' for (let index = 0; index < 90; index += 1) { pastedTail += `${'pasted text '.repeat(25)}${index}\n` } const splitSpy = vi.spyOn(String.prototype, 'split') runtime.onPtyData( 'pty-bg', [ 'Antigravity CLI 1.0.3\n', 'user@example.com (Antigravity Business)\n', pastedTail, 'Gemini 4 Experimental (High)\n', '~/orca/workspaces/orca/agy-dispatch-issue\n', '>' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: true, status: 'running' }) const splitReadyTail = splitSpy.mock.contexts.some((context) => { const value = typeof context === 'string' ? context : String(context) return value.includes('antigravity cli') && value.includes('pasted text pasted text') }) expect(splitReadyTail).toBe(false) }) it('resolves tui-idle from an Antigravity prompt before the model line', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Do you trust this workspace directory?\n', 'Press t to trust\n', antigravityPromptBeforeModelReadyScreen('Gemini 3.5 Flash (High)') ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: true, status: 'running' }) }) it('resolves live-leaf tui-idle from an Antigravity ready prompt preview', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: null } ] }) runtime.onPtyData('pty-1', antigravityReadyScreen(), Date.now()) const [terminal] = (await runtime.listTerminals()).terminals await expect( runtime.waitForTerminal(terminal.handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle: terminal.handle, condition: 'tui-idle', status: 'running' }) }) it('resolves tui-idle from a Codex ready prompt even when stale startup lines remain', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Booting MCP server: computer-use(0s esc to interrupt)\n', ' >_ OpenAI Codex (v0.132.0)\n', ' model: gpt-5.5 high /model to change\n', ' directory: ~/orca/workspaces/orca/cli-debug\n', [ 'Starting MCP servers (0/2): codex_apps, computer-use (2s esc to interrupt)', 'Run /review on my current changes gpt-5.5 high ~/orca/workspaces/orca/cli-debug', 'Run /review on my current changes gpt-5.5 high ~/orca/workspaces/orca/cli-debug', 'Run /review on my current changes gpt-5.5 high ~/orca/workspaces/orca/cli-debug', 'Run /review on my current changes gpt-5.5 high ~/orca/workspaces/orca/cli-debug\n' ].join('') ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: true, status: 'running' }) }) it('resolves tui-idle when a stale Codex prompt is followed by Antigravity readiness', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Do you trust this workspace directory?\n', 'Press t to trust\n', antigravityReadyScreen(), '\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: true, status: 'running' }) }) it('resolves tui-idle when a stale Codex prompt is followed by the ready header', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Choose working directory to resume this session\n', 'Press enter to continue\n', ' >_ OpenAI Codex (v0.132.0)\n', ' model: gpt-5.5 high /model to change\n', ' directory: ~/orca/workspaces/orca/cli-debug\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: true, status: 'running' }) }) it('blocks tui-idle when a newer prompt follows a stale prompt and ready header', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Update available! 0.131.0 -> 0.132.0\n', 'Press enter to continue\n', ' >_ OpenAI Codex (v0.132.0)\n', ' model: gpt-5.5 high /model to change\n', ' directory: ~/orca/workspaces/orca/cli-debug\n', 'Hooks need review\n', 'Press enter to confirm\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: false, status: 'running', blockedReason: 'codex-hooks-review-prompt' }) }) it('returns a blocked wait result for Codex update prompts', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Update available! 0.131.0 -> 0.132.0\n', '1. Update now\n', '2. Skip\n', 'Press enter to continue\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: false, status: 'running', blockedReason: 'codex-update-prompt' }) }) it('returns a blocked wait result for Codex workspace trust prompts', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', 'Do you trust this workspace directory?\n1. Yes\n2. No\n', Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: false, status: 'running', blockedReason: 'codex-trust-workspace' }) }) it('resolves tui-idle for an idle Cursor lane past its dismissed trust dialog (#8210)', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'cursor-agent' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) // Cursor's dismissed trust dialog stays in scrollback; the later idle prompt must clear that stale hit and satisfy idle. runtime.onPtyData( 'pty-bg', [ // Trust dialog mentions "Cursor Agent" before the ready banner; lastIndexOf must pick the later banner, not this hit. 'Cursor Agent\n', '⚠ Workspace Trust Required\n', 'Do you trust the contents of this directory?\n', ' ▶ [a] Trust this workspace\n', ' [q] Quit\n', cursorReadyScreen() ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: true, status: 'running' }) }) it('does not block a mid-run Cursor lane on its dismissed trust dialog (#8210)', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'cursor-agent' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ // Same earlier "Cursor Agent" hit as the idle case — banner must win. 'Cursor Agent\n', '⚠ Workspace Trust Required\n', 'Do you trust the contents of this directory?\n', ' ▶ [a] Trust this workspace\n', ' [q] Quit\n', cursorBusyScreen() ].join(''), Date.now() ) // Busy Cursor is neither blocked nor idle, so the wait times out honestly instead of returning a stale trust block. await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 200 }) ).rejects.toThrow('timeout') }) it('returns a blocked wait result for Codex cwd selection prompts', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Choose working directory to resume this session\n', ' Session = latest cwd recorded in the resumed session\n', ' Current = your current working directory\n', ' Press enter to continue\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: false, status: 'running', blockedReason: 'codex-cwd-prompt' }) }) it('returns a blocked wait result for Codex model migration prompts', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Codex just got an upgrade. Introducing gpt-5.1-codex-max.\n', 'We recommend switching from gpt-5-codex to gpt-5.1-codex-max.\n', 'Press enter to continue\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: false, status: 'running', blockedReason: 'codex-model-migration-prompt' }) }) it('returns a blocked wait result for Codex startup hook review prompts', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Hooks need review\n', '2 hooks are new or changed.\n', '1. Review hooks\n', '2. Trust all and continue\n', 'Press enter to confirm or esc to go back\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: false, status: 'running', blockedReason: 'codex-hooks-review-prompt' }) }) it('returns a blocked wait result for generic Codex interactive prompts', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData( 'pty-bg', [ 'Would you like to grant these permissions?\n', '1. Yes, grant these permissions for this turn\n', '2. No, continue without permissions\n', 'Press enter to confirm or esc to cancel\n' ].join(''), Date.now() ) await expect( runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) ).resolves.toMatchObject({ handle, condition: 'tui-idle', satisfied: false, status: 'running', blockedReason: 'codex-interactive-prompt' }) }) it('does not classify unrelated press-enter prompts as Codex blocked prompts', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData('pty-bg', 'Press enter to continue\n', Date.now()) const waitPromise = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 }) const timeoutAssertion = expect(waitPromise).rejects.toThrow('timeout') await vi.advanceTimersByTimeAsync(2_000) await timeoutAssertion } finally { vi.useRealTimers() } }) it('resolves tui-idle for quiet background PTY agents without OSC titles', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.onPtyData('pty-bg', 'OpenAI Codex\n', Date.now()) const waitPromise = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 10_000 }) const waitAssertion = expect(waitPromise).resolves.toMatchObject({ handle, condition: 'tui-idle', status: 'running' }) await vi.advanceTimersByTimeAsync(6_000) await waitAssertion } finally { vi.useRealTimers() } }) it('splits text and enter writes for background terminal handles', async () => { const writes: string[] = [] const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) await runtime.sendTerminal(handle, { text: 'continue', enter: true }) expect(writes).toEqual(['continue', '\r']) }) it('sends agent prompts as bracketed paste before submit', async () => { vi.useFakeTimers() try { const writes: string[] = [] const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const prompt = 'line one\nline two\x1b[201~' const sendPromise = runtime.sendTerminalAgentPrompt(handle, prompt) await vi.runAllTimersAsync() const result = await sendPromise const pasted = [ AGENT_PROMPT_BRACKETED_PASTE_START, 'line one\nline two[201~', AGENT_PROMPT_BRACKETED_PASTE_END ].join('') expect(result).toMatchObject({ handle, accepted: true, bytesWritten: Buffer.byteLength(`${pasted}\r`, 'utf8') }) expect(writes).toEqual([pasted, '\r']) } finally { vi.useRealTimers() } }) it('chunks large agent prompt paste frames before delayed submit', async () => { vi.useFakeTimers() try { const writes: string[] = [] const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const prompt = `${'x'.repeat(TERMINAL_INPUT_CHUNK_MAX_BYTES)}\ntail` const sendPromise = runtime.sendTerminalAgentPrompt(handle, prompt) await vi.runAllTimersAsync() const result = await sendPromise const pasteWrites = writes.slice(0, -1) expect(result.bytesWritten).toBe( Buffer.byteLength(`${buildAgentPromptPasteBytes(prompt)}\r`, 'utf8') ) expect(writes.at(-1)).toBe('\r') expect(pasteWrites.length).toBeGreaterThan(1) expect(pasteWrites.join('')).toBe(buildAgentPromptPasteBytes(prompt)) expect(pasteWrites[0]).toContain(AGENT_PROMPT_BRACKETED_PASTE_START) expect(pasteWrites.at(-1)).toContain(AGENT_PROMPT_BRACKETED_PASTE_END) } finally { vi.useRealTimers() } }) it('closes an incomplete agent prompt paste when a later chunk write fails', async () => { vi.useFakeTimers() try { const writes: string[] = [] let writeCount = 0 const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writeCount += 1 writes.push(data) return writeCount !== 2 }, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const prompt = 'x'.repeat(TERMINAL_INPUT_CHUNK_MAX_BYTES + 1) const sendPromise = runtime.sendTerminalAgentPrompt(handle, prompt) const sendRejection = expect(sendPromise).rejects.toThrow('terminal_not_writable') await vi.runAllTimersAsync() await sendRejection expect(writes[0]).toContain(AGENT_PROMPT_BRACKETED_PASTE_START) expect(writes.at(-1)).toBe(AGENT_PROMPT_BRACKETED_PASTE_END) expect(writes).not.toContain('\r') } finally { vi.useRealTimers() } }) it('chunks large terminal.send text before provider writes', async () => { const writes: string[] = [] const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const text = ['x'.repeat(TERMINAL_INPUT_CHUNK_MAX_BYTES), 'tail'].join('') const result = await runtime.sendTerminal(handle, { text }) expect(result).toMatchObject({ handle, accepted: true, bytesWritten: Buffer.byteLength(text, 'utf8') }) expect(writes).toEqual(['x'.repeat(TERMINAL_INPUT_CHUNK_MAX_BYTES), 'tail']) }) it('yields while validating accepted large terminal.send text before provider writes', async () => { const writes: string[] = [] const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const text = 'é'.repeat(CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS + 1) vi.useFakeTimers() try { const sendPromise = runtime.sendTerminal(handle, { text }) expect(writes).toEqual([]) await vi.runAllTimersAsync() const result = await sendPromise expect(result).toMatchObject({ handle, accepted: true, bytesWritten: Buffer.byteLength(text, 'utf8') }) expect(writes.length).toBeGreaterThan(1) expect(writes.join('')).toBe(text) } finally { vi.useRealTimers() } }) it('rejects oversized terminal.send text before provider writes', async () => { const writes: string[] = [] const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) await expect( runtime.sendTerminal(handle, { text: 'x'.repeat(TERMINAL_INPUT_MAX_BYTES + 1) }) ).rejects.toThrow(TERMINAL_INPUT_TOO_LARGE_ERROR) expect(writes).toEqual([]) }) it('reveals a background terminal session when focusing its handle', async () => { const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-adopted' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { title: 'worker' }) await expect(runtime.focusTerminal(handle)).resolves.toMatchObject({ handle, tabId: 'tab-adopted', worktreeId: TEST_WORKTREE_ID }) // Why: focus reveal must reuse createTerminal's pre-minted tabId so a retry adopts under the paneKey baked into env. expect(revealTerminalSession).toHaveBeenLastCalledWith(TEST_WORKTREE_ID, { ptyId: 'pty-bg', title: 'worker', tabId: expect.stringMatching(UUID_RE), leafId: expect.stringMatching(UUID_RE) }) }) it('replays captured launch config when focusing a background agent session', async () => { const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-adopted' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', launchAgent: 'codex', launchConfig: { agentCommand: 'codex', agentArgs: '--model gpt-5', agentEnv: { CODEX_PROFILE: 'captured' } }, title: 'worker' }) const firstReveal = revealTerminalSession.mock.calls[0]?.[1] as | { launchToken?: string; tabId?: string; leafId?: string } | undefined revealTerminalSession.mockClear() await runtime.focusTerminal(handle) expect(revealTerminalSession).toHaveBeenCalledWith(TEST_WORKTREE_ID, { ptyId: 'pty-bg', title: 'worker', launchConfig: { agentCommand: 'codex', agentArgs: '--model gpt-5', agentEnv: { CODEX_PROFILE: 'captured' } }, launchToken: firstReveal?.launchToken, launchAgent: 'codex', tabId: firstReveal?.tabId, leafId: firstReveal?.leafId }) }) it('reveals background terminal sessions with the freshest PTY title', async () => { const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-adopted' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { title: 'Claude working' }) runtime.onPtyData('pty-bg', '\x1b]0;claude agents\x07', 100) await runtime.focusTerminal(handle) expect(revealTerminalSession).toHaveBeenLastCalledWith( TEST_WORKTREE_ID, expect.objectContaining({ ptyId: 'pty-bg', title: 'claude agents' }) ) }) it('rejects focusing an exited background terminal session', async () => { const revealTerminalSession = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) revealTerminalSession.mockClear() runtime.onPtyExit('pty-bg', 0) await expect(runtime.focusTerminal(handle)).rejects.toThrow('terminal_exited') expect(revealTerminalSession).not.toHaveBeenCalled() }) it('renames background terminal handles without requiring a visible tab', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) const renamed = await runtime.renameTerminal(handle, 'Worker') expect(renamed).toMatchObject({ handle, title: 'Worker' }) expect(renamed.tabId).not.toContain(':') await expect(runtime.showTerminal(handle)).resolves.toMatchObject({ tabId: renamed.tabId, title: 'Worker' }) }) it('keeps a background terminal handle stable while reveal adoption is racing', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { title: 'worker' }) await runtime.focusTerminal(handle) ;(runtime as unknown as { handleByPtyId: Map }).handleByPtyId.delete('pty-bg') await expect(runtime.showTerminal(handle)).resolves.toMatchObject({ handle, ptyId: 'pty-bg' }) }) it('clears terminal scrollback through the PTY controller and headless buffer', async () => { const clearBuffer = vi.fn().mockResolvedValue(undefined) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, clearBuffer }) syncSinglePty(runtime, 'pty-1') runtime.onPtyData( 'pty-1', `${Array.from({ length: 20 }, (_, i) => `line-${i}`).join('\n')}\n`, 123 ) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.clearTerminalBuffer(terminal.handle)).resolves.toEqual({ handle: terminal.handle, cleared: true }) expect(clearBuffer).toHaveBeenCalledWith('pty-1') const snapshot = await runtime.serializeTerminalBuffer('pty-1', { scrollbackRows: 1000 }) expect(snapshot?.data).not.toContain('line-0') }) 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 reads exclude partial lines to prevent duplication (partial now, then completed line 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('paginates retained terminal output with explicit limits and truncation metadata', 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', `${Array.from({ length: 150 }, (_, index) => `line-${index}`).join('\n')}\n`, 100 ) const preview = await runtime.readTerminal(terminal.handle) expect(preview.tail).toHaveLength(120) expect(preview.tail[0]).toBe('line-30') expect(preview.limited).toBe(true) expect(preview.oldestCursor).toBe('0') expect(preview.latestCursor).toBe('150') const defaultCursorRead = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(defaultCursorRead.tail).toHaveLength(150) expect(defaultCursorRead.nextCursor).toBe('150') expect(defaultCursorRead.limited).toBe(false) const firstPage = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 50 }) expect(firstPage.tail).toHaveLength(50) expect(firstPage.tail[0]).toBe('line-0') expect(firstPage.nextCursor).toBe('50') expect(firstPage.limited).toBe(true) expect(firstPage.truncated).toBe(false) const fractionalPage = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 0.5 }) expect(fractionalPage.tail).toEqual(['line-0']) expect(fractionalPage.nextCursor).toBe('1') expect(fractionalPage.limited).toBe(true) const secondPage = await runtime.readTerminal(terminal.handle, { cursor: Number(firstPage.nextCursor), limit: 200 }) expect(secondPage.tail).toHaveLength(100) expect(secondPage.tail[0]).toBe('line-50') expect(secondPage.nextCursor).toBe('150') expect(secondPage.limited).toBe(false) runtime.onPtyData( 'pty-1', `${Array.from({ length: 2100 }, (_, index) => `later-${index}`).join('\n')}\n`, 101 ) const staleCursorRead = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 5 }) expect(staleCursorRead.truncated).toBe(true) expect(staleCursorRead.oldestCursor).toBe('250') expect(staleCursorRead.tail).toEqual([ 'later-100', 'later-101', 'later-102', 'later-103', 'later-104' ]) expect(staleCursorRead.nextCursor).toBe('255') const futureCursorRead = await runtime.readTerminal(terminal.handle, { cursor: 9999 }) expect(futureCursorRead.tail).toEqual([]) expect(futureCursorRead.nextCursor).toBe('2250') expect(futureCursorRead.limited).toBe(false) }) // Why: PR #2553 keeps retained output cursor-reachable while previews stay bounded (not full-transcript payloads). it('keeps terminal read payloads bounded while retained output remains pageable', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals const linePayload = 'x'.repeat(24) const lines = Array.from( { length: 2000 }, (_, index) => `line-${index.toString().padStart(4, '0')}-${linePayload}` ) runtime.onPtyData('pty-1', `${lines.join('\n')}\n`, 100) const preview = await runtime.readTerminal(terminal.handle) expect(Buffer.byteLength(JSON.stringify(preview), 'utf8')).toBeLessThan(10_000) expect(preview.tail).toHaveLength(120) expect(preview.tail[0]).toBe(lines.at(-120)) expect(preview.limited).toBe(true) expect(preview.oldestCursor).toBe('0') expect(preview.nextCursor).toBe('2000') expect(preview.latestCursor).toBe('2000') const collected: string[] = [] let cursor = Number(preview.oldestCursor) const latestCursor = Number(preview.latestCursor) for (let pageIndex = 0; cursor < latestCursor; pageIndex += 1) { expect(pageIndex).toBeLessThan(10) const page = await runtime.readTerminal(terminal.handle, { cursor, limit: 333 }) expect(Buffer.byteLength(JSON.stringify(page), 'utf8')).toBeLessThan(16_000) expect(page.tail.length).toBeGreaterThan(0) expect(page.tail.length).toBeLessThanOrEqual(333) expect(page.returnedLineCount).toBe(page.tail.length) collected.push(...page.tail) const nextCursor = Number(page.nextCursor) expect(nextCursor).toBeGreaterThan(cursor) cursor = nextCursor } expect(collected).toHaveLength(lines.length) expect(collected.findIndex((line, index) => line !== lines[index])).toBe(-1) }) it('trims terminal read preview character budget without per-line array shifts', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals const lines = Array.from({ length: 120 }, (_, index) => `line-${index}-${'x'.repeat(400)}`) runtime.onPtyData('pty-1', `${lines.join('\n')}\n`, 100) // Why: xterm-headless uses Array.shift while draining writes; this test guards read-preview trimming, not emulator parsing. await runtime.serializeMainTerminalBuffer('pty-1') const originalShift = Array.prototype.shift let shiftCallCount = 0 Array.prototype.shift = function (...args) { shiftCallCount += 1 return originalShift.apply(this, args) } let preview: Awaited> try { preview = await runtime.readTerminal(terminal.handle) } finally { Array.prototype.shift = originalShift } expect(preview.limited).toBe(true) expect(preview.tail.at(-1)).toBe(lines.at(-1)) expect(preview.tail.reduce((sum, line) => sum + line.length, 0)).toBeLessThanOrEqual(32 * 1024) expect(shiftCallCount).toBe(0) }) it('falls back to renderer visible screen when uncursored TUI tail is blank', async () => { const serializeBuffer = vi.fn().mockResolvedValue({ data: '\x1b[?1049hClaude Code\r\nWorking on fix\r\nTool: Read\r\n', cols: 80, rows: 24 }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, hasRendererSerializer: () => true, serializeBuffer }) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', `${Array.from({ length: 3000 }, () => ' ').join('\n')}\n`, 100) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['Claude Code', 'Working on fix', 'Tool: Read']) expect(serializeBuffer).toHaveBeenCalledWith('pty-1', { scrollbackRows: 0, altScreenForcesZeroRows: false }) }) it('reads and shows the runtime-owned alternate-screen grid without serialization', async () => { const serializeBuffer = vi.fn() const serializeProviderBuffer = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, serializeProviderBuffer }) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData( 'pty-1', 'shell history\r\n\x1b[?1049h\x1b[2J\x1b[HClaude Code\r\nWorking on fix\r\nTool: Read\r\n', 100 ) const read = await runtime.readTerminal(terminal.handle) const shown = await runtime.showTerminal(terminal.handle) expect(read.tail).toEqual(['Claude Code', 'Working on fix', 'Tool: Read']) expect(shown.preview).toBe('Claude Code\nWorking on fix\nTool: Read') expect(serializeBuffer).not.toHaveBeenCalled() expect(serializeProviderBuffer).not.toHaveBeenCalled() }) it('classifies older provider alternate-screen snapshots from ANSI', async () => { const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: '\x1b[?1049h\x1b[2J\x1b[HRemote Vim\r\nediting README.md\r\n', cols: 80, rows: 24, seq: 900, source: 'headless' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['Remote Vim', 'editing README.md']) expect(runtime.isTerminalAlternateScreen('pty-1')).toBe(true) expect(serializeProviderBuffer).toHaveBeenCalledOnce() }) it('honors the last ANSI screen transition when older provider metadata is absent', async () => { const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: '\x1b[?1049hOld TUI\r\n\x1b[?1049l', cols: 80, rows: 24, seq: 900, source: 'headless' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.onPtyData('pty-1', 'shell history\r\n', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['shell history']) expect(runtime.isTerminalAlternateScreen('pty-1')).toBe(false) }) it.each([ { name: 'live exit wins over an alternate provider snapshot', snapshotData: '\x1b[?1049h\x1b[2J\x1b[HStale TUI\r\n', snapshotMode: true, liveData: '\x1b[?1049lreturned shell\r\n', expectedMode: false, expectedLine: 'shell history' }, { name: 'live entry wins over a primary provider snapshot', snapshotData: 'stale shell\r\n', snapshotMode: false, liveData: '\x1b[?1049h\x1b[2J\x1b[HLive TUI\r\nnew frame\r\n', expectedMode: true, expectedLine: 'Live TUI' } ])('$name', async ({ snapshotData, snapshotMode, liveData, expectedMode, expectedLine }) => { type Snapshot = { data: string cols: number rows: number seq: number source: 'headless' alternateScreen: boolean } let resolveSnapshot!: (snapshot: Snapshot) => void const serializeProviderBuffer = vi.fn( () => new Promise((resolve) => { resolveSnapshot = resolve }) ) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.onPtyData('pty-1', 'shell history\r\n', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const readPromise = runtime.readTerminal(terminal.handle) await vi.waitFor(() => expect(serializeProviderBuffer).toHaveBeenCalledOnce()) runtime.onPtyData('pty-1', liveData, 101) resolveSnapshot({ data: snapshotData, cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: snapshotMode }) const read = await readPromise expect(runtime.isTerminalAlternateScreen('pty-1')).toBe(expectedMode) expect(read.tail.join('\n')).toContain(expectedLine) expect(read.tail.join('\n')).not.toContain('Stale TUI') }) it('shares one provider snapshot between concurrent read and show calls', async () => { type Snapshot = { data: string cols: number rows: number seq: number source: 'headless' alternateScreen: boolean } let resolveSnapshot!: (snapshot: Snapshot) => void const serializeProviderBuffer = vi.fn( () => new Promise((resolve) => { resolveSnapshot = resolve }) ) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const readPromise = runtime.readTerminal(terminal.handle) const showPromise = runtime.showTerminal(terminal.handle) await vi.waitFor(() => expect(serializeProviderBuffer).toHaveBeenCalledOnce()) resolveSnapshot({ data: '\x1b[?1049h\x1b[2J\x1b[HShared TUI frame\r\n', cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: true }) const [read, shown] = await Promise.all([readPromise, showPromise]) expect(read.tail).toEqual(['Shared TUI frame']) expect(shown.preview).toBe('Shared TUI frame') expect(serializeProviderBuffer).toHaveBeenCalledOnce() }) it('keeps cursor reads provider-free on restored alternate-screen PTYs', async () => { const serializeProviderBuffer = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.onPtyData('pty-1', 'shell history\r\n', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const read = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(read.tail).toEqual(['shell history']) expect(serializeProviderBuffer).not.toHaveBeenCalled() }) it('backs off after a provider snapshot failure instead of retrying for show', async () => { const serializeProviderBuffer = vi.fn().mockRejectedValue(new Error('provider unavailable')) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.onPtyData('pty-1', 'shell history\r\n', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const read = await runtime.readTerminal(terminal.handle) const shown = await runtime.showTerminal(terminal.handle) expect(read.tail).toEqual(['shell history']) expect(shown.preview).toBe('shell history') expect(serializeProviderBuffer).toHaveBeenCalledOnce() }) it('falls back once to a mounted renderer when a known alternate provider fails', async () => { const serializeProviderBuffer = vi .fn() .mockResolvedValueOnce({ data: '\x1b[?1049h\x1b[2J\x1b[HProvider TUI\r\n', cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: true }) .mockRejectedValue(new Error('provider unavailable')) const serializeBuffer = vi.fn().mockResolvedValue({ data: '\x1b[?1049h\x1b[2J\x1b[HRenderer TUI\r\n', cols: 80, rows: 24 }) let rendererMounted = false const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeBuffer, serializeProviderBuffer, hasRendererSerializer: () => rendererMounted }) syncSinglePty(runtime) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.readTerminal(terminal.handle)).resolves.toMatchObject({ tail: ['Provider TUI'] }) runtime.onPtyData('pty-1', 'new output', 100) rendererMounted = true const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['Renderer TUI']) expect(serializeProviderBuffer).toHaveBeenCalledTimes(2) expect(serializeBuffer).toHaveBeenCalledOnce() }) it('bounds a stuck provider snapshot and shares the timed-out request', async () => { vi.useFakeTimers() try { const serializeProviderBuffer = vi.fn(() => new Promise(() => undefined)) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.onPtyData('pty-1', 'shell history\r\n', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const reads = Promise.all([ runtime.readTerminal(terminal.handle), runtime.showTerminal(terminal.handle) ]) await vi.advanceTimersByTimeAsync(750) const [read, shown] = await reads expect(read.tail).toEqual(['shell history']) expect(shown.preview).toBe('shell history') expect(serializeProviderBuffer).toHaveBeenCalledOnce() } finally { vi.useRealTimers() } }) it('discards a provider frame when the PTY generation resets during capture', async () => { type Snapshot = { data: string cols: number rows: number seq: number source: 'headless' alternateScreen: boolean } let resolveSnapshot!: (snapshot: Snapshot) => void const serializeProviderBuffer = vi.fn( () => new Promise((resolve) => { resolveSnapshot = resolve }) ) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.onPtyData('pty-1', 'old shell\r\n', 100) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const readPromise = runtime.readTerminal(terminal.handle) await vi.waitFor(() => expect(serializeProviderBuffer).toHaveBeenCalledOnce()) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 0, generation: 'reset' }, runtime.getPtyOutputSequence('pty-1') ) resolveSnapshot({ data: '\x1b[?1049hStale generation TUI\r\n', cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: true }) const read = await readPromise expect(read.tail.join('\n')).not.toContain('Stale generation TUI') expect(runtime.isTerminalAlternateScreen('pty-1')).toBe(false) }) it('rejects a visible frame when its terminal handle changes during capture', async () => { type Snapshot = { data: string cols: number rows: number seq: number source: 'headless' alternateScreen: boolean } let resolveSnapshot!: (snapshot: Snapshot) => void const serializeProviderBuffer = vi.fn( () => new Promise((resolve) => { resolveSnapshot = resolve }) ) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const readPromise = runtime.readTerminal(terminal.handle) await vi.waitFor(() => expect(serializeProviderBuffer).toHaveBeenCalledOnce()) syncSinglePty(runtime, 'pty-2') resolveSnapshot({ data: '\x1b[?1049hWrong terminal frame\r\n', cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: true }) await expect(readPromise).rejects.toThrow('terminal_handle_stale') }) it('bounds provider visible frames for read and show responses', async () => { const visibleLines = [ 'skip-1', 'skip-2', 'A'.repeat(49), 'B'.repeat(49), 'C'.repeat(49), 'D'.repeat(49), 'E'.repeat(49), 'F'.repeat(50) ] const expectedTail = visibleLines.slice(-6) const serializeProviderBuffer = vi.fn().mockResolvedValue({ data: `\x1b[?1049h\x1b[2J\x1b[H${visibleLines.join('\r\n')}\r\n`, cols: 80, rows: 24, seq: 900, source: 'headless', alternateScreen: true }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, serializeProviderBuffer, hasRendererSerializer: () => false }) syncSinglePty(runtime) runtime.synchronizePtyOutputSequenceFromProvider( 'pty-1', { value: 900, generation: 'continued' }, 0 ) const [terminal] = (await runtime.listTerminals()).terminals const read = await runtime.readTerminal(terminal.handle, { limit: 6 }) const shown = await runtime.showTerminal(terminal.handle) expect(read.tail).toEqual(expectedTail) expect(read.returnedLineCount).toBe(6) expect(shown.preview).toBe(expectedTail.join('\n')) expect(shown.preview.length).toBe(300) expect(serializeProviderBuffer).toHaveBeenCalledOnce() }) it('returns renderer visible screen lines through terminal.read RPC JSON result', async () => { const serializeBuffer = vi.fn().mockResolvedValue({ data: '\x1b[?1049hClaude Code\r\nChecking files\r\nWaiting for input\r\n', cols: 80, rows: 24 }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, hasRendererSerializer: () => true, serializeBuffer }) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', `${Array.from({ length: 3000 }, () => '').join('\n')}\n`, 100) const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS }) const response = await dispatcher.dispatch( makeRpcRequest('terminal.read', { terminal: terminal.handle }) ) expect(response.ok).toBe(true) if (!response.ok) { throw new Error(response.error.message) } expect(response.result).toMatchObject({ terminal: { handle: terminal.handle, status: 'running', tail: ['Claude Code', 'Checking files', 'Waiting for input'] } }) }) it('does not use renderer visible-screen fallback for cursor transcript reads', async () => { const serializeBuffer = vi.fn().mockResolvedValue({ data: 'Visible TUI\n', cols: 80, rows: 24 }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, hasRendererSerializer: () => true, serializeBuffer }) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', ' \n', 100) const read = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(read.tail).toEqual(['']) expect(serializeBuffer).not.toHaveBeenCalledWith('pty-1', { scrollbackRows: 0, altScreenForcesZeroRows: false }) }) it('does not use renderer visible-screen fallback for a short blank shell tail', async () => { const serializeBuffer = vi.fn().mockResolvedValue({ data: 'shell prompt\n', cols: 80, rows: 24 }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, hasRendererSerializer: () => true, serializeBuffer }) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '\n\n', 100) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['', '']) expect(serializeBuffer).not.toHaveBeenCalledWith('pty-1', { scrollbackRows: 0, altScreenForcesZeroRows: false }) }) it('trims oversized terminal output bursts without per-line array shifts', async () => { const shiftSpy = vi.spyOn(Array.prototype, 'shift') const lines = Array.from({ length: 5000 }, (_, index) => `line-${index}`) const result = appendNormalizedToTailBuffer([], '', `${lines.join('\n')}\n`) const shiftCallCount = shiftSpy.mock.calls.length shiftSpy.mockRestore() expect(result.truncated).toBe(true) expect(result.lines).toHaveLength(2000) expect(result.lines.slice(0, 5)).toEqual([ 'line-3000', 'line-3001', 'line-3002', 'line-3003', 'line-3004' ]) expect(shiftCallCount).toBe(0) }) it('trims terminal tail character budget without per-line array shifts', () => { const shiftSpy = vi.spyOn(Array.prototype, 'shift') const lines = Array.from({ length: 1000 }, (_, index) => `line-${index}-${'x'.repeat(300)}`) const result = appendNormalizedToTailBuffer([], '', `${lines.join('\n')}\n`) const shiftCallCount = shiftSpy.mock.calls.length shiftSpy.mockRestore() let retainedChars = lines.reduce((sum, line) => sum + line.length, 0) let expectedStartIndex = 0 while (expectedStartIndex < lines.length && retainedChars > 256 * 1024) { retainedChars -= lines[expectedStartIndex].length expectedStartIndex += 1 } expect(result.truncated).toBe(true) expect(result.lines).toEqual(lines.slice(expectedStartIndex)) expect(result.lines.reduce((sum, line) => sum + line.length, 0)).toBeLessThanOrEqual(256 * 1024) expect(shiftCallCount).toBe(0) }) it('builds terminal previews without mapping the full retained tail', () => { const lines = Array.from({ length: 5000 }, (_, index) => index % 2 === 0 ? `line-${index}` : ' ' ) const mapSpy = vi.spyOn(Array.prototype, 'map') const preview = buildPreview(lines, 'partial-tail') const mapCallCount = mapSpy.mock.calls.length mapSpy.mockRestore() expect(preview).toBe( ['line-4990', 'line-4992', 'line-4994', 'line-4996', 'line-4998', 'partial-tail'].join('\n') ) expect(mapCallCount).toBe(0) }) it('keeps recent PTY replay output capped without needing previous data for large chunks', () => { const previous = 'old-output'.repeat(1000) const data = 'new-output'.repeat(1000) const outputLimit = 64 * 1024 const smallTail = new RecentPtyOutputBuffer() smallTail.append(previous) smallTail.append('tail') expect(smallTail.read()).toBe(`${previous}tail`.slice(-outputLimit)) const combined = new RecentPtyOutputBuffer() combined.append(previous) combined.append(data) expect(combined.read()).toBe(`${previous}${data}`.slice(-outputLimit)) const fresh = new RecentPtyOutputBuffer() fresh.append(data) expect(fresh.read()).toBe(data.slice(-outputLimit)) }) it('keeps mobile-visible artifact paths in bounded PTY path candidates', () => { const artifactPath = '/tmp/result-visible-in-mobile-scrollback.json' const prefix = 'x'.repeat(8 * 1024) const candidates = appendRecentPtyPathCandidates(undefined, `${artifactPath}\n${prefix}`) expect(candidates.length).toBeGreaterThan(0) expect(recentTerminalPathCandidatesIncludePath(candidates, artifactPath, artifactPath)).toBe( true ) }) it('keeps dotted spaced artifact paths in bounded PTY path candidates', () => { const artifactPath = '/tmp/v1.2 reports/result.json' const prefix = 'x'.repeat(8 * 1024) const candidates = appendRecentPtyPathCandidates(undefined, `wrote ${artifactPath}\n${prefix}`) expect(recentTerminalPathCandidatesIncludePath(candidates, artifactPath, artifactPath)).toBe( true ) expect(recentTerminalPathCandidatesIncludePath(candidates, '/tmp/v1.2', '/tmp/v1.2')).toBe( false ) }) it('does not swallow trailing prose ending in a filename into candidates', () => { const candidates = appendRecentPtyPathCandidates( undefined, '/tmp/app.log failed to start app.py\n' ) expect( recentTerminalPathCandidatesIncludePath(candidates, '/tmp/app.log', '/tmp/app.log') ).toBe(true) expect( recentTerminalPathCandidatesIncludePath( candidates, '/tmp/app.log failed to start app.py', '/tmp/app.log failed to start app.py' ) ).toBe(false) }) it('bounds candidate extraction cost on pathological separator floods', () => { const flood = '/'.repeat(64 * 1024) const start = performance.now() const candidates = appendRecentPtyPathCandidates(undefined, flood) const elapsed = performance.now() - start expect(candidates).toEqual([]) // Why: the extension regex is quadratic per line; unbounded it took seconds on the PTY hot path. Loose bound to avoid CI flake. expect(elapsed).toBeLessThan(500) }) it('strips retained terminal path line and hash locators before matching', () => { const colonPath = '/tmp/orca report/result.json' const hashPath = '/tmp/result-hash.json' const candidates = appendRecentPtyPathCandidates( undefined, `wrote ${colonPath}:12:3 for you\nfile://${hashPath}#L12C3 generated\n` ) expect(recentTerminalPathCandidatesIncludePath(candidates, colonPath, colonPath)).toBe(true) expect(recentTerminalPathCandidatesIncludePath(candidates, hashPath, hashPath)).toBe(true) }) it('keeps non-loopback file URI authorities in retained PTY path candidates', () => { const candidates = appendRecentPtyPathCandidates( undefined, 'wrote file://remote-host/tmp/result.json\n' ) expect( recentTerminalPathCandidatesIncludePath( candidates, '//remote-host/tmp/result.json', '//remote-host/tmp/result.json' ) ).toBe(true) expect( recentTerminalPathCandidatesIncludePath(candidates, '/tmp/result.json', '/tmp/result.json') ).toBe(false) }) it('keeps loopback file URI paths as local retained PTY path candidates', () => { const candidates = appendRecentPtyPathCandidates( undefined, 'wrote file://localhost/tmp/result.json#L12\n' ) expect( recentTerminalPathCandidatesIncludePath(candidates, '/tmp/result.json', '/tmp/result.json') ).toBe(true) }) it('keeps tmpdir artifact paths in bounded PTY path candidates', () => { const artifactPath = join(tmpdir(), 'orca-runtime-retained-result.json') const candidates = appendRecentPtyPathCandidates(undefined, `wrote ${artifactPath}\n`) expect(recentTerminalPathCandidatesIncludePath(candidates, artifactPath, artifactPath)).toBe( true ) }) it('matches WSL UNC artifact paths against POSIX terminal output candidates', () => { const candidates = appendRecentPtyPathCandidates(undefined, 'wrote /tmp/result.json\n') expect( recentTerminalPathCandidatesIncludePath( candidates, '\\\\wsl.localhost\\Ubuntu\\tmp\\result.json', '\\\\wsl.localhost\\Ubuntu\\tmp\\result.json' ) ).toBe(true) }) it('keeps bounded PTY path candidates under a total byte budget', () => { const longCandidate = `/tmp/${'x'.repeat(5 * 1024)}.json` const candidates = appendRecentPtyPathCandidates(undefined, `${longCandidate}\n`) expect(candidates).toEqual([]) let retained: string[] | undefined for (let index = 0; index < 200; index += 1) { retained = appendRecentPtyPathCandidates(retained, `/tmp/${'a'.repeat(900)}-${index}.json\n`) } const totalBytes = (retained ?? []).reduce( (sum, candidate) => sum + Buffer.byteLength(candidate, 'utf8'), 0 ) expect(totalBytes).toBeLessThanOrEqual(64 * 1024) }) it('keeps Windows file URI drive paths in bounded PTY path candidates', () => { const artifactPath = 'C:/Users/me/AppData/Local/Temp/result.json' const prefix = 'x'.repeat(8 * 1024) const candidates = appendRecentPtyPathCandidates( undefined, `file:///C:/Users/me/AppData/Local/Temp/result.json\n${prefix}` ) expect(recentTerminalPathCandidatesIncludePath(candidates, artifactPath, artifactPath)).toBe( true ) }) it('matches terminal artifact paths only when they appear in recent terminal output', () => { expect( recentTerminalOutputIncludesPath( 'wrote /tmp/orca report/result.json:12:3', '/tmp/orca report/result.json', '/tmp/orca report/result.json' ) ).toBe(true) expect( recentTerminalOutputIncludesPath( '\x1b]8;;file:///tmp/orca%20report/result.json\x1b\\result\x1b]8;;\x1b\\', '/tmp/orca report/result.json', '/tmp/orca report/result.json' ) ).toBe(true) expect( recentTerminalOutputIncludesPath( '\x1b]8;;file:///tmp/caf%C3%A9.txt\x1b\\result\x1b]8;;\x1b\\', '/tmp/café.txt', '/tmp/café.txt' ) ).toBe(true) expect( recentTerminalOutputIncludesPath( 'wrote /tmp/orca report/other.json', '/tmp/orca report/result.json', '/tmp/orca report/result.json' ) ).toBe(false) }) it('does not match terminal artifact path prefixes as provenance', () => { expect( recentTerminalOutputIncludesPath( 'wrote /tmp/result.json.bak', '/tmp/result.json', '/tmp/result.json' ) ).toBe(false) expect( recentTerminalOutputIncludesPath( 'wrote /tmp/result.json:12:3', '/tmp/result.json', '/tmp/result.json' ) ).toBe(true) }) it('matches terminal artifact paths inside loopback file URI output', () => { expect( recentTerminalOutputIncludesPath( 'wrote file://127.0.0.1/tmp/result.json', '/tmp/result.json', '/tmp/result.json' ) ).toBe(true) expect( recentTerminalOutputIncludesPath( 'wrote file://[::1]/tmp/result.json', '/tmp/result.json', '/tmp/result.json' ) ).toBe(true) expect( recentTerminalOutputIncludesPath( 'wrote file:///C:/Users/me/AppData/Local/Temp/result.json', 'C:/Users/me/AppData/Local/Temp/result.json', 'C:/Users/me/AppData/Local/Temp/result.json' ) ).toBe(true) expect( recentTerminalOutputIncludesPath( 'wrote file://localhost/C:/Users/me/AppData/Local/Temp/result.json', 'C:/Users/me/AppData/Local/Temp/result.json', 'C:/Users/me/AppData/Local/Temp/result.json' ) ).toBe(true) }) it('applies terminal redraw controls before retaining previews', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'Working\rWorking 1s\rWorking 2s', 100) const carriageRead = await runtime.readTerminal(terminal.handle) expect(carriageRead.tail).toEqual(['Working 2s']) expect(carriageRead.latestCursor).toBe('0') runtime.onPtyData('pty-1', '\b\b3s', 101) const backspaceRead = await runtime.readTerminal(terminal.handle) expect(backspaceRead.tail).toEqual(['Working 3s']) expect(backspaceRead.latestCursor).toBe('0') runtime.onPtyData('pty-1', '\rDone\n', 102) const completedRead = await runtime.readTerminal(terminal.handle) expect(completedRead.tail).toEqual(['Done']) expect(completedRead.latestCursor).toBe('1') }) it('applies ANSI terminal redraw controls before retaining previews', async () => { const cursorRedraw = appendNormalizedToTailBuffer([], '', 'Working 10%\x1b[3D25%') expect(cursorRedraw.partialLine).toBe('Working 25%') const eraseLineKeepsCursor = appendNormalizedToTailBuffer([], '', 'ABC\x1b[2KXY\n') expect(eraseLineKeepsCursor.lines).toEqual([' XY']) const eraseWithoutCarriageReturn = appendNormalizedToTailBuffer( [], 'Downloading 10%', '\x1b[2K\x1b[1GDownloading 20%' ) expect(eraseWithoutCarriageReturn.partialLine).toBe('Downloading 20%') const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData( 'pty-1', 'Working\r\x1b[2K\x1b[1G\x1b[?25l\x1b[32mDone\x1b[0m\x1b]0;title\u0007\n', 100 ) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['Done']) expect(read.latestCursor).toBe('1') }) it('retains ANSI-only status redraws instead of appending every frame', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '• Working', 100) const initialPreview = await runtime.readTerminal(terminal.handle) expect(initialPreview.tail).toEqual(['• Working']) runtime.onPtyData('pty-1', '\x1b[2K\x1b[1G• Working.', 101) const redrawPreview = await runtime.readTerminal(terminal.handle) expect(redrawPreview.tail).toEqual(['• Working.']) expect(redrawPreview.tail.join('\n')).not.toContain('• Working• Working') runtime.onPtyData('pty-1', '\x1b[2K\x1b[1G• Working..', 102) const latestPreview = await runtime.readTerminal(terminal.handle) expect(latestPreview.tail).toEqual(['• Working..']) expect(latestPreview.latestCursor).toBe('0') const cursorReadBeforeNewline = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(cursorReadBeforeNewline.tail).toEqual([]) expect(cursorReadBeforeNewline.nextCursor).toBe('0') runtime.onPtyData('pty-1', '\n', 103) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['• Working..']) expect(read.latestCursor).toBe('1') expect(read.tail.join('\n')).not.toContain('• Working• Working') const cursorReadAfterNewline = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(cursorReadAfterNewline.tail).toEqual(['• Working..']) expect(cursorReadAfterNewline.nextCursor).toBe('1') }) it('retains multi-line ANSI footer redraws instead of appending old frames', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '• Working\nTool call\n', 100) runtime.onPtyData( 'pty-1', '\x1b[2A\x1b[2K\x1b[1G• Working.\n\x1b[2K\x1b[1GTool call finished\n', 101 ) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['• Working.', 'Tool call finished']) expect(read.latestCursor).toBe('4') expect(read.tail.join('\n')).not.toContain('• Working\nTool call\n• Working.') expect(read.tail.join('\n')).not.toContain('2A') expect(read.tail.join('\n')).not.toContain('2K') }) it('keeps the cursor column when erasing full lines in multi-line redraws', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'ABC\nxyz', 100) runtime.onPtyData('pty-1', '\x1b[1A\x1b[2KXY\n', 101) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual([' XY']) expect(read.tail.join('\n')).not.toContain('ABCXY') expect(read.tail.join('\n')).not.toContain('2K') }) it('retains split multi-line ANSI footer redraw state across chunks', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '• Working\nTool call\n', 100) const beforeRedraw = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(beforeRedraw.tail).toEqual(['• Working', 'Tool call']) expect(beforeRedraw.nextCursor).toBe('2') runtime.onPtyData('pty-1', '\x1b[2A', 101) const betweenChunks = await runtime.readTerminal(terminal.handle) expect(betweenChunks.tail).toEqual(['• Working']) expect(betweenChunks.latestCursor).toBe('2') runtime.onPtyData('pty-1', '\x1b[2K\x1b[1G• Working.\n\x1b[2K\x1b[1GTool call finished\n', 102) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['• Working.', 'Tool call finished']) expect(read.latestCursor).toBe('4') const cursorRead = await runtime.readTerminal(terminal.handle, { cursor: Number(beforeRedraw.nextCursor) }) expect(cursorRead.tail).toEqual(['• Working.', 'Tool call finished']) expect(cursorRead.oldestCursor).toBe('0') expect(cursorRead.nextCursor).toBe('4') }) it('does not let stale lower rows hide earlier corrected footer rows from cursor reads', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'A\nB\nC\n', 100) const beforeRedraw = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(beforeRedraw.tail).toEqual(['A', 'B', 'C']) expect(beforeRedraw.nextCursor).toBe('3') runtime.onPtyData('pty-1', '\x1b[2A\x1b[2K\x1b[1GB2\n', 101) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['A', 'B2']) expect(read.latestCursor).toBe('4') const cursorRead = await runtime.readTerminal(terminal.handle, { cursor: Number(beforeRedraw.nextCursor) }) expect(cursorRead.tail).toEqual(['B2']) expect(cursorRead.oldestCursor).toBe('0') expect(cursorRead.nextCursor).toBe('4') }) it('keeps cursor pagination stable across a CUU redraw of the live screen', async () => { const runtime = new OrcaRuntimeService(store) const liveScreen = new HeadlessEmulator({ cols: 80, rows: 24 }) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals const initialOutput = 'A\r\nB\r\nC\r\n' const redraw = '\x1b[2A\x1b[2K\x1b[1GB2\r\n' runtime.onPtyData('pty-1', initialOutput, 100) await liveScreen.write(initialOutput) const firstPage = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 2 }) expect(firstPage).toMatchObject({ tail: ['A', 'B'], truncated: false, limited: true, oldestCursor: '0', nextCursor: '2', latestCursor: '3', returnedLineCount: 2 }) runtime.onPtyData('pty-1', redraw, 101) await liveScreen.write(redraw) // Why: CUU changes the mutable screen, but completed-line pagination is a distinct contract. expect(liveScreen.getVisibleLines().filter((line) => line.length > 0)).toEqual(['A', 'B2', 'C']) await expect(runtime.readTerminal(terminal.handle, { cursor: 2 })).resolves.toMatchObject({ tail: ['C', 'B2'], truncated: false, limited: false, oldestCursor: '0', nextCursor: '4', latestCursor: '4', returnedLineCount: 2 }) liveScreen.dispose() }) it('records completed transcript lines before later CUU redraws in the same PTY chunk', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'A\r\nB\r\nC\r\n\x1b[2A\x1b[2K\x1b[1GB2\r\n', 100) await expect(runtime.readTerminal(terminal.handle, { cursor: 0 })).resolves.toMatchObject({ tail: ['A', 'B', 'C', 'B2'], truncated: false, oldestCursor: '0', nextCursor: '4', latestCursor: '4' }) }) it('bounds completed transcript entries from a single multi-line redraw chunk', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals const lines = Array.from({ length: 2100 }, (_, index) => `redraw-${index}`) runtime.onPtyData('pty-1', `\x1b[1A${lines.join('\n')}\n`, 100) await expect( runtime.readTerminal(terminal.handle, { cursor: 0, limit: 3 }) ).resolves.toMatchObject({ tail: ['redraw-100', 'redraw-101', 'redraw-102'], truncated: true, oldestCursor: '100', nextCursor: '103', latestCursor: '2100' }) }) it('retains multi-line ANSI redraws when the last footer row stays partial', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '• Working\nTool call\n', 100) const beforeRedraw = await runtime.readTerminal(terminal.handle, { cursor: 0 }) expect(beforeRedraw.nextCursor).toBe('2') runtime.onPtyData( 'pty-1', '\x1b[2A\x1b[2K\x1b[1G• Working.\n\x1b[2K\x1b[1GTool call still running', 101 ) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['• Working.', 'Tool call still running']) expect(read.latestCursor).toBe('3') const cursorRead = await runtime.readTerminal(terminal.handle, { cursor: Number(beforeRedraw.nextCursor) }) expect(cursorRead.tail).toEqual(['• Working.']) expect(cursorRead.oldestCursor).toBe('0') expect(cursorRead.nextCursor).toBe('3') runtime.onPtyData('pty-1', '\n', 102) const completedPartialRead = await runtime.readTerminal(terminal.handle, { cursor: 3 }) expect(completedPartialRead.tail).toEqual(['Tool call still running']) expect(completedPartialRead.nextCursor).toBe('4') }) it('does not retain split ANSI controls as visible terminal preview text', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'Working\r\x1b[', 100) runtime.onPtyData('pty-1', '38;2;190;210;223;49mWo', 101) const colorRead = await runtime.readTerminal(terminal.handle) const colorRetained = colorRead.tail.join('\n') expect(colorRetained).toContain('Wo') expect(colorRetained).not.toContain('38;2') expect(colorRetained).not.toContain('49m') runtime.onPtyData('pty-1', 'rking\x1b[?2026', 102) runtime.onPtyData('pty-1', 'l', 103) const modeRead = await runtime.readTerminal(terminal.handle) const retained = modeRead.tail.join('\n') expect(retained).toContain('Working') expect(retained).not.toContain('38;2') expect(retained).not.toContain('?2026') expect(retained).not.toContain('49m') runtime.onPtyData('pty-1', ` done\x1b]0;${'x'.repeat(5000)}`, 104) runtime.onPtyData('pty-1', '\u0007\n', 105) const longRead = await runtime.readTerminal(terminal.handle) const longRetained = longRead.tail.join('\n') expect(longRetained).toContain('Working done') expect(longRetained).not.toContain('x'.repeat(100)) const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('x'.repeat(MAX_OSC_TITLE_CHARS)) }) it('applies ANSI split redraw controls without leaking raw params', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'Frame old', 100) runtime.onPtyData('pty-1', '\x1b[', 101) const pendingEraseRead = await runtime.readTerminal(terminal.handle) expect(pendingEraseRead.tail).toEqual(['Frame old']) expect(pendingEraseRead.tail.join('\n')).not.toContain('[') runtime.onPtyData('pty-1', '2K\x1b[', 102) const pendingColumnRead = await runtime.readTerminal(terminal.handle) expect(pendingColumnRead.tail.join('\n')).not.toContain('2K') expect(pendingColumnRead.tail.join('\n')).not.toContain('1G') runtime.onPtyData('pty-1', '1GFrame new\n', 103) const read = await runtime.readTerminal(terminal.handle) const retained = read.tail.join('\n') expect(read.tail).toEqual(['Frame new']) expect(retained).not.toContain('2K') expect(retained).not.toContain('1G') }) it('retains same-line redraw cursor position across split full-line erase chunks', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'ABC', 100) runtime.onPtyData('pty-1', '\x1b[2K', 101) const erasedRead = await runtime.readTerminal(terminal.handle) expect(erasedRead.tail).toEqual([]) runtime.onPtyData('pty-1', 'XY\n', 102) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual([' XY']) expect(read.tail.join('\n')).not.toContain('ABCXY') expect(read.tail.join('\n')).not.toContain('2K') }) it('keeps huge ANSI cursor movement params bounded in retained previews', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals const infinityParam = '9'.repeat(400) runtime.onPtyData('pty-1', `A\x1b[1000000000CZ\n`, 100) runtime.onPtyData('pty-1', `B\x1b[${infinityParam}GQ\n`, 101) for (let index = 0; index < 50; index += 1) { runtime.onPtyData('pty-1', `R${index}\x1b[2K\x1b[999999CZ\n`, 102 + index) } const read = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 60 }) expect(read.tail).toHaveLength(52) for (const line of read.tail) { expect(line.length).toBeLessThan(5000) expect(line).not.toContain('1000000000') expect(line).not.toContain(infinityParam) expect(line).not.toContain('999999') } expect(read.tail[0]?.startsWith('A')).toBe(true) expect(read.tail[0]?.endsWith('Z')).toBe(true) expect(read.tail[1]?.startsWith('B')).toBe(true) expect(read.tail[1]?.endsWith('Q')).toBe(true) expect(read.tail.at(-1)?.endsWith('Z')).toBe(true) }) it('bounds retained work for many newline-separated huge ANSI cursor movements', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '\x1b[4000GZ\n'.repeat(3000), 100) const read = await runtime.readTerminal(terminal.handle, { cursor: 0, limit: 2000 }) expect(read.latestCursor).toBe('3000') expect(read.oldestCursor).not.toBe('0') expect(read.tail.length).toBeLessThan(100) for (const line of read.tail) { expect(line.length).toBeLessThanOrEqual(4000) expect(line.endsWith('Z')).toBe(true) expect(line).not.toContain('4000G') } }) it('applies ANSI erase-from-start line controls in retained previews', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'ABCDE\x1b[3D\x1b[1KXY\n', 100) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual([' XYE']) expect(read.tail.join('\n')).not.toContain('ABC') expect(read.tail.join('\n')).not.toContain('1K') }) it('applies ANSI stripping for private or intermediate CSI line controls', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'ABCDE\x1b[?99DXY\n', 100) runtime.onPtyData('pty-1', 'ABCDE\x1b[1$DXY\n', 101) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['ABCDEXY', 'ABCDEXY']) expect(read.tail.join('\n')).not.toContain('?99D') expect(read.tail.join('\n')).not.toContain('1$D') }) it('applies ANSI stripping for unsupported erase-line modes', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'Old\x1b[3KNew\n', 100) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['OldNew']) expect(read.tail.join('\n')).not.toContain('3K') }) it('does not retain split ST-terminated string controls as preview text', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', 'Before \x1b_Gi=31337,s=1,', 100) runtime.onPtyData('pty-1', 'v=1,a=q,t=d,f=24;AAAA\x1b\\After\n', 101) const read = await runtime.readTerminal(terminal.handle) const retained = read.tail.join('\n') expect(retained).toContain('BeforeAfter') expect(retained).not.toContain('Gi=31337') expect(retained).not.toContain('AAAA') }) it('preserves non-ASCII terminal preview text in chunks with controls', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '\x1b[32mHéllo 🌊\x1b[0m\n', 100) const read = await runtime.readTerminal(terminal.handle) expect(read.tail).toEqual(['Héllo 🌊']) }) it('detects split OSC titles before retaining terminal previews', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex work', 100) runtime.onPtyData('pty-1', 'ing\x07Visible\n', 101) const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('Codex working') expect(pty?.lastAgentStatus).toBe('working') const [terminal] = (await runtime.listTerminals()).terminals const read = await runtime.readTerminal(terminal.handle) expect(read.tail.join('\n')).toContain('Visible') expect(read.tail.join('\n')).not.toContain('Codex working') }) it('detects ST-terminated OSC titles split before the final backslash', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x1b', 100) runtime.onPtyData('pty-1', '\\Visible\n', 101) const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('Codex working') expect(pty?.lastAgentStatus).toBe('working') }) it('preserves a trailing escape after a completed OSC title', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07\x1b', 100) runtime.onPtyData('pty-1', ']0;Codex done\x07Visible\n', 101) const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('Codex done') expect(pty?.lastAgentStatus).toBe('idle') }) it('normalizes rotating Grok working-frame OSC titles to one stable stored title', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;⠋ - Waiting for response… - grok\x07', 100) const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('⠋ Grok') expect(pty?.lastAgentStatus).toBe('working') // A different rotating frame must store an identical title — title equality is what stops per-frame session-tab and mobile-snapshot touch. runtime.onPtyData('pty-1', '\x1b]0;⠴ - Thinking - grok\x07', 101) expect(pty?.lastOscTitle).toBe('⠋ Grok') expect(pty?.lastAgentStatus).toBe('working') }) it('does not republish mobile session tabs for same-status Grok title frames', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) events.length = 0 runtime.onPtyData('laptop-created-pty', '\x1b]0;⠋ - Waiting for response… - grok\x07', 100) runtime.onPtyData('laptop-created-pty', '\x1b]0;⠴ - Thinking - grok\x07', 101) runtime.onPtyData('laptop-created-pty', '\x1b]0;⠙ - Responding - grok\x07', 102) await waitForMobileSessionTabsEvents(events, 1) expect(events).toHaveLength(1) expect(events[0]?.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: '⠋ Grok', agentStatus: expect.objectContaining({ state: 'working' }) }) ) unsubscribe() }) // #7970: headless serve has no renderer syncing tab.agentStatus, so hook-only transitions must republish the snapshot carrying the retained hook payload. it('republishes mobile session tabs with hook payloads for title-less OSC 9999 transitions', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'hook-only-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'hook-tab', leafId: HEADLESS_LEAF_ID }) events.length = 0 runtime.onPtyData( 'hook-only-pty', '\x1b]9999;{"state":"working","prompt":"fix the tests","agentType":"opencode"}\x07', 100 ) await waitForMobileSessionTabsEvents(events, 1) expect(events).toHaveLength(1) expect(events[0]?.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', agentStatus: expect.objectContaining({ state: 'working', prompt: 'fix the tests', agentType: 'opencode' }) }) ) runtime.onPtyData( 'hook-only-pty', '\x1b]9999;{"state":"waiting","prompt":"fix the tests","agentType":"opencode"}\x07', 101 ) await waitForMobileSessionTabsEvents(events, 2) expect(events).toHaveLength(2) expect(events[1]?.tabs[0]?.type === 'terminal' && events[1].tabs[0].agentStatus).toEqual( expect.objectContaining({ state: 'waiting' }) ) unsubscribe() }) // Why: restored OMP panes can retain the hook while the wrapped Pi owns foreground (#6364). it('keeps an OMP hook labeled OMP when the wrapped pi child owns the foreground', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'omp-flicker-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, // Why: the remote relay reads the deeper `pi` child of the omp process tree. getForegroundProcess: async () => 'pi' }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'omp-tab', leafId: HEADLESS_LEAF_ID }) // Restored/mirrored pane: no launchAgent, only the pi foreground read remains. const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('omp-flicker-pty')! pty.launchAgent = null pty.foregroundAgent = 'pi' events.length = 0 runtime.onPtyData( 'omp-flicker-pty', '\x1b]0;⠋ Pi\x07' + '\x1b]9999;{"state":"working","prompt":"fix the bug","agentType":"omp"}\x07', 100 ) await waitForMobileSessionTabsEvents(events, 1) expect(events[0]?.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: '⠋ OMP', agentStatus: expect.objectContaining({ state: 'working', agentType: 'omp', terminalTitle: '⠋ OMP' }) }) ) unsubscribe() }) it('does not republish mobile session tabs for repeated identical OSC 9999 payloads', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'hook-ping-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'hook-ping-tab', leafId: HEADLESS_LEAF_ID }) events.length = 0 const payload = '\x1b]9999;{"state":"working","prompt":"same","agentType":"codex"}\x07' runtime.onPtyData('hook-ping-pty', payload, 100) runtime.onPtyData('hook-ping-pty', payload, 101) runtime.onPtyData('hook-ping-pty', payload, 102) await waitForMobileSessionTabsEvents(events, 1) expect(events).toHaveLength(1) unsubscribe() }) it('suppresses a retained hook working status once the shell owns the pane title again', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'hook-exit-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'hook-exit-tab', leafId: HEADLESS_LEAF_ID }) events.length = 0 runtime.onPtyData( 'hook-exit-pty', '\x1b]9999;{"state":"working","prompt":"long task","agentType":"codex"}\x07', 100 ) // Agent exits without a hook done event and the shell takes the title back; the stuck-spinner guard (#1437) must win over the retained hook row. runtime.onPtyData('hook-exit-pty', '\x1b]0;zsh\x07', 101) await waitForMobileSessionTabsEvents(events, 1) const last = events.at(-1)?.tabs[0] expect(last?.type).toBe('terminal') expect(last?.type === 'terminal' ? last.agentStatus : null).toBeFalsy() unsubscribe() }) it('stores normalized Pi idle OSC titles that still classify as idle', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;π - my-project\x07', 100) const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('Pi') expect(pty?.lastAgentStatus).toBe('idle') // Why: worktree.ps / mobile re-detect from stored lastOscTitle, not the raw OSC frame; bare "Pi" must still classify as idle after normalize. expect(detectAgentStatusFromTitle(pty?.lastOscTitle ?? '')).toBe('idle') }) it('normalizes hydration-seeded Grok and Pi titles the same as live OSC frames', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) ;( runtime as unknown as { applySeededAgentStatus: (ptyId: string, title: string) => void } ).applySeededAgentStatus('pty-1', '⠴ - Thinking - grok') const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('⠋ Grok') // Seed writes leaf status only; re-detect from the stored title must still report working so later live frames compare equal and don't thrash. expect(detectAgentStatusFromTitle(pty?.lastOscTitle ?? '')).toBe('working') ;( runtime as unknown as { applySeededAgentStatus: (ptyId: string, title: string) => void } ).applySeededAgentStatus('pty-1', 'π - my-project') expect(pty?.lastOscTitle).toBe('Pi') expect(detectAgentStatusFromTitle(pty?.lastOscTitle ?? '')).toBe('idle') }) it('stores other-agent OSC titles that merely end in grok unchanged', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) runtime.onPtyData('pty-1', '\x1b]0;⠋ wire up grok\x07', 100) const pty = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(pty?.lastOscTitle).toBe('⠋ wire up grok') // Claude/Codex braille + task ending " - grok" is not a Grok frame shape. runtime.onPtyData('pty-1', '\x1b]0;⠋ fix the flaky suite - grok\x07', 101) expect(pty?.lastOscTitle).toBe('⠋ fix the flaky suite - grok') }) it('seeds newly synced leaves from PTY pending ANSI state', async () => { const runtime = new OrcaRuntimeService(store) runtime.registerPty('pty-1', TEST_WORKTREE_ID) runtime.onPtyData('pty-1', 'Working\r\x1b[', 100) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '38;2;190;210;223;49mDone\n', 101) const read = await runtime.readTerminal(terminal.handle) const retained = read.tail.join('\n') expect(retained).toContain('Done') expect(retained).not.toContain('38;2') expect(retained).not.toContain('49m') }) it('normalizes large CRLF-heavy terminal chunks without regex replacement or line splits', async () => { const replaceSpy = vi.spyOn(String.prototype, 'replace') const splitSpy = vi.spyOn(String.prototype, 'split') const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', `${'line\r\n'.repeat(10_000)}tail`, 100) const read = await runtime.readTerminal(terminal.handle, { limit: 5 }) const usedCrlfReplace = replaceSpy.mock.calls.some( ([pattern], index) => pattern instanceof RegExp && pattern.source === '\\r\\n' && typeof replaceSpy.mock.contexts[index] === 'string' && replaceSpy.mock.contexts[index].length > 10_000 ) const usedLineSplit = splitSpy.mock.calls.some(([separator], index) => { const splitSeparator = separator as unknown return ( (splitSeparator === '\n' || (splitSeparator instanceof RegExp && splitSeparator.source === '\\r?\\n')) && typeof splitSpy.mock.contexts[index] === 'string' && splitSpy.mock.contexts[index].length > 10_000 ) }) expect(read.tail.at(-1)).toBe('tail') expect(usedCrlfReplace).toBe(false) expect(usedLineSplit).toBe(false) }) it('bounds retained partial terminal output before preview reads', 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', `${Array.from({ length: 2000 }, (_, index) => `line-${index}`).join('\n')}\n`, 99 ) runtime.onPtyData('pty-1', `${'x'.repeat(40_000)}tail-marker-0`, 100) type RetainedTailState = { tailBuffer: string[] tailPartialLine: string tailTruncated: boolean } const cappedPartialState = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') const retainedLineBuffer = cappedPartialState?.tailBuffer for (let index = 1; index < 5; index += 1) { runtime.onPtyData('pty-1', `${'x'.repeat(40_000)}tail-marker-${index}`, 100 + index) } const retained = ( runtime as unknown as { ptysById: Map } ).ptysById.get('pty-1') expect(retained?.tailBuffer).toBe(retainedLineBuffer) expect(retained?.tailPartialLine).toHaveLength(4000) expect(retained?.tailPartialLine.endsWith('tail-marker-4')).toBe(true) expect(retained?.tailTruncated).toBe(true) const preview = await runtime.readTerminal(terminal.handle) expect(preview.tail).toHaveLength(120) expect(preview.tail.at(-1)).toHaveLength(4000) expect(preview.tail.at(-1)?.endsWith('tail-marker-4')).toBe(true) expect(preview.truncated).toBe(true) expect(preview.nextCursor).toBe('2000') }) it('delivers pending orchestration messages to an already-idle agent', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const db = new InMemoryOrchestrationMessages() const write = vi.fn().mockReturnValue(true) setInMemoryOrchestrationMessages(runtime, 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.setActiveCoordinatorRun({ coordinator_handle: 'term_other' }) db.insertMessage({ from: 'term_sender', to: terminal.handle, subject: 'hello' }) runtime.deliverPendingMessagesForHandle(terminal.handle) expect(write).toHaveBeenCalledWith('pty-1', expect.stringContaining('Subject: hello')) // Why: the split Enter write lands after the 500ms delay; advance past it before asserting on delivered_at. await vi.advanceTimersByTimeAsync(500) expect(write).toHaveBeenCalledWith('pty-1', '\r') // Why: design doc §3.2 splits delivered vs. read — push-on-idle stamps delivered_at but must not flip read; only the agent's check consumes, so rows stay unread. const unread = db.getUnreadMessages(terminal.handle) expect(unread).toHaveLength(1) expect(unread[0].read).toBe(0) expect(unread[0].delivered_at).not.toBeNull() db.close() } finally { vi.useRealTimers() } }) it('injects pending orchestration messages into the active coordinator without auto-submitting', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const db = new InMemoryOrchestrationMessages() const write = vi.fn().mockReturnValue(true) setInMemoryOrchestrationMessages(runtime, 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.setActiveCoordinatorRun({ coordinator_handle: terminal.handle }) db.insertMessage({ from: 'term_sender', to: terminal.handle, subject: 'hello coordinator' }) runtime.deliverPendingMessagesForHandle(terminal.handle) expect(write).toHaveBeenCalledWith( 'pty-1', expect.stringContaining('Subject: hello coordinator') ) await vi.advanceTimersByTimeAsync(500) const submitWrites = write.mock.calls.filter( ([ptyId, text]) => ptyId === 'pty-1' && text === '\r' ) expect(submitWrites).toHaveLength(0) const unread = db.getUnreadMessages(terminal.handle) expect(unread).toHaveLength(1) expect(unread[0].read).toBe(0) expect(unread[0].delivered_at).not.toBeNull() db.close() } finally { vi.useRealTimers() } }) it('injects pending orchestration messages into Cursor Agent without auto-submitting', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const db = new InMemoryOrchestrationMessages() const write = vi.fn().mockReturnValue(true) setInMemoryOrchestrationMessages(runtime, db) runtime.setPtyController({ write, kill: vi.fn(), getForegroundProcess: async () => null }) syncSinglePty(runtime) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '\x1b]0;\u280b Cursor Agent\x07', 100) runtime.onPtyData('pty-1', '\x1b]0;Cursor ready\x07', 101) db.insertMessage({ from: 'term_sender', to: terminal.handle, subject: 'hello cursor' }) runtime.deliverPendingMessagesForHandle(terminal.handle) expect(write).toHaveBeenCalledWith('pty-1', expect.stringContaining('Subject: hello cursor')) await vi.advanceTimersByTimeAsync(500) const submitWrites = write.mock.calls.filter( ([ptyId, text]) => ptyId === 'pty-1' && text === '\r' ) expect(submitWrites).toHaveLength(0) // Why: Cursor Agent treats injected PTY text as editable prompt input, so the user submits manually but the banner must not replay on the next idle transition. const unread = db.getUnreadMessages(terminal.handle) expect(unread).toHaveLength(1) expect(unread[0].read).toBe(0) expect(unread[0].delivered_at).not.toBeNull() db.close() } finally { vi.useRealTimers() } }) it('still auto-submits to a non-Cursor agent when its idle title mentions Cursor Agent', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const db = new InMemoryOrchestrationMessages() const write = vi.fn().mockReturnValue(true) setInMemoryOrchestrationMessages(runtime, db) runtime.setPtyController({ write, kill: vi.fn(), getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1', { tabTitle: 'cursor-repro-branch' }) const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '\x1b]0;. Investigate Cursor Agent\x07', 100) runtime.onPtyData('pty-1', '\x1b]0;* Investigate Cursor Agent\x07', 101) db.insertMessage({ from: 'term_sender', to: terminal.handle, subject: 'hello claude' }) runtime.deliverPendingMessagesForHandle(terminal.handle) await vi.advanceTimersByTimeAsync(500) expect(write).toHaveBeenCalledWith('pty-1', expect.stringContaining('Subject: hello claude')) expect(write).toHaveBeenCalledWith('pty-1', '\r') db.close() } finally { vi.useRealTimers() } }) it('does not replay an already-delivered message on a later idle transition', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const db = new InMemoryOrchestrationMessages() const write = vi.fn().mockReturnValue(true) setInMemoryOrchestrationMessages(runtime, 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) await vi.advanceTimersByTimeAsync(500) const firstInjections = write.mock.calls.filter( (c) => typeof c[1] === 'string' && c[1].includes('Subject: hello') ).length expect(firstInjections).toBe(1) // Second idle transition: the row is unread but already delivered, so push-on-idle must skip it to avoid the replay bug. runtime.deliverPendingMessagesForHandle(terminal.handle) await vi.advanceTimersByTimeAsync(500) const totalInjections = write.mock.calls.filter( (c) => typeof c[1] === 'string' && c[1].includes('Subject: hello') ).length expect(totalInjections).toBe(1) db.close() } finally { 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('recovers exported ORCA_TERMINAL_HANDLE from discovered live PTY sessions', 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-1', cwd: TEST_WORKTREE_PATH, title: 'claude', terminalHandle: 'term_exported' } ] }) const listed = await runtime.listTerminals() expect(listed.terminals[0]?.handle).toBe('term_exported') runtime.onPtyData('pty-1', 'after restart\n', 100) await expect(runtime.readTerminal('term_exported')).resolves.toMatchObject({ handle: 'term_exported', tail: ['after restart'] }) await expect( runtime.sendTerminal('term_exported', { text: 'still writable' }) ).resolves.toMatchObject({ handle: 'term_exported', accepted: true }) expect(writes).toEqual(['still writable']) }) it('does not adopt a discovered terminal handle already bound to another live PTY', async () => { const runtime = new OrcaRuntimeService(store) const writesByPty = new Map() runtime.setPtyController({ write: (ptyId, data) => { writesByPty.set(ptyId, [...(writesByPty.get(ptyId) ?? []), data]) return true }, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'pty-victim', cwd: TEST_WORKTREE_PATH, title: 'claude', terminalHandle: 'term_victim' }, { id: 'pty-imposter', cwd: TEST_WORKTREE_PATH, title: 'claude', terminalHandle: 'term_victim' } ] }) const listed = await runtime.listTerminals() const handles = listed.terminals.map((terminal) => terminal.handle) expect(handles).toContain('term_victim') expect(new Set(handles).size).toBe(handles.length) await expect( runtime.sendTerminal('term_victim', { text: 'for victim' }) ).resolves.toMatchObject({ accepted: true }) expect(writesByPty.get('pty-victim')).toEqual(['for victim']) expect(writesByPty.has('pty-imposter')).toBe(false) }) it('keeps an already-bound terminal handle when discovery reports a different exported one', 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-1', cwd: TEST_WORKTREE_PATH, title: 'claude', terminalHandle: 'term_from_env' } ] }) runtime.registerPreAllocatedHandleForPty('pty-1', 'term_already_bound') const listed = await runtime.listTerminals() expect(listed.terminals[0]?.handle).toBe('term_already_bound') await expect( runtime.sendTerminal('term_already_bound', { text: 'still routed' }) ).resolves.toMatchObject({ accepted: true }) expect(writes).toEqual(['still routed']) // the reported-but-not-adopted handle must not resolve to the live pty await expect(runtime.readTerminal('term_from_env')).rejects.toThrow() }) it('binds advertised URLs for renderer-restored PTYs that skip registerPty', () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime, 'pty-restored') runtime.onPtyData('pty-restored', 'Network: https://restored.example.com:3001/\n', 100) expect(advertisedUrlWatcher.lookup(TEST_WORKTREE_ID, 3001)?.origin).toBe( 'https://restored.example.com:3001' ) }) 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 runtime-created PTY handles valid after graph unavailable', async () => { const writes: string[] = [] const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`) runtime.markGraphUnavailable(1) runtime.onPtyData('pty-bg', 'after unavailable\n', 100) await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ handle, tail: ['after unavailable'] }) await expect(runtime.sendTerminal(handle, { text: 'still writable' })).resolves.toMatchObject({ handle, accepted: true }) expect(writes).toEqual(['still writable']) }) it('recognizes runtime-created PTY handles with agent launch titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', title: 'Codex package-cache cleanup' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('does not recognize runtime-created Claude agents management screens as agents', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('uses stale runtime-created PTY status when there is no title or foreground evidence', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude' }) const pty = ( runtime as unknown as { ptysById: Map< string, { lastAgentStatus: 'working' | null } > } ).ptysById.get('pty-bg') expect(pty).toBeDefined() if (!pty) { throw new Error('expected runtime PTY record') } pty.lastAgentStatus = 'working' runtime.setPtyController(null) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('lets Claude agents management titles clear stale runtime-created title status', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) const pty = ( runtime as unknown as { ptysById: Map< string, { lastAgentStatus: 'working' | null lastOscTitle: string | null lastOscTitleAt: number | null } > } ).ptysById.get('pty-bg') expect(pty).toBeDefined() if (!pty) { throw new Error('expected runtime PTY record') } pty.lastAgentStatus = 'working' pty.lastOscTitle = 'claude agents' pty.lastOscTitleAt = 0 await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('does not recognize live Claude agents panes from a Claude foreground process', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude agents' }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(false) }) it('lets Claude agents pane titles override stale live-leaf title status', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude working' }) runtime.onPtyData('pty-1', '\x1b]0;claude working\x07', 100) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude agents' }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(false) }) it('lets Claude agents OSC titles override stale live-leaf pane titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude working' }) runtime.onPtyData('pty-1', '\x1b]0;claude agents\x07', 100) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(false) }) it('does not let stale tab-level Claude agents titles suppress current pane activity', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) syncSinglePty(runtime, 'pty-1', { tabTitle: 'claude agents', paneTitle: 'claude working' }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(true) }) it('does not let stale tab-level agent titles override current neutral pane titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1', { tabTitle: 'claude working', paneTitle: 'bash' }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(false) }) it('does not let stale live-leaf status override current neutral pane titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude working' }) runtime.onPtyData('pty-1', '\x1b]0;claude working\x07', 100) syncSinglePty(runtime, 'pty-1', { paneTitle: 'bash' }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(false) }) it('does not expose stale live-leaf agent status after Claude agents title supersedes it', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude working' }) runtime.onPtyData('pty-1', '\x1b]0;claude working\x07', 100) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude agents' }) const [terminal] = (await runtime.listTerminals()).terminals expect(runtime.getAgentStatusForHandle(terminal.handle)).toBeNull() }) it('lists live terminals with fresh pane titles over stale tab titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'claude working', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'claude agents' } ] }) const [terminal] = (await runtime.listTerminals()).terminals expect(terminal.title).toBe('claude agents') }) it('does not let stale Claude agents OSC titles suppress current pane activity', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude agents' }) runtime.onPtyData('pty-1', '\x1b]0;claude agents\x07', 100) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude working' }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(true) }) it('lets adopted pane Claude agents titles override stale PTY-handle activity', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude', title: 'claude working' }) runtime.onPtyData('pty-bg', '\x1b]0;claude working\x07', 100) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'claude agents' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('lets adopted neutral pane titles override stale PTY-handle activity', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude', title: 'claude working' }) runtime.onPtyData('pty-bg', '\x1b]0;claude working\x07', 100) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'bash' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('lets adopted neutral pane titles use non-shell foreground fallback', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'bash', title: 'bash' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'bash' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('lets adopted neutral pane titles retry wrapper foregrounds until recognized', async () => { const getForegroundProcess = vi .fn() .mockResolvedValueOnce('node') .mockResolvedValueOnce('codex') const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'bash', title: 'bash' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'bash' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) expect(getForegroundProcess).toHaveBeenCalledTimes(2) }) it('waits for delayed wrapper foreground cache enrichment', async () => { const getForegroundProcess = vi.fn(async () => (Date.now() >= 4_000 ? 'codex' : 'node')) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'bash', title: 'bash' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'bash' }) vi.useFakeTimers() vi.setSystemTime(0) try { const result = runtime.isTerminalRunningAgent(handle) await vi.advanceTimersByTimeAsync(4_200) await expect(result).resolves.toBe(true) expect(getForegroundProcess.mock.calls.length).toBeGreaterThan(20) } finally { vi.useRealTimers() } }) it('does not recognize arbitrary foreground TUIs as running agents', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'vim' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'bash', title: 'bash' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'bash' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('does not recognize unresolved wrapper foregrounds as running agents', async () => { const getForegroundProcess = vi.fn().mockResolvedValue('node') const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'bash', title: 'bash' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'bash' }) vi.useFakeTimers() try { const result = runtime.isTerminalRunningAgent(handle) await vi.advanceTimersByTimeAsync(7_000) await expect(result).resolves.toBe(false) expect(getForegroundProcess.mock.calls.length).toBeGreaterThan(2) } finally { vi.useRealTimers() } }) it('lets live neutral pane titles retry wrapper foregrounds until recognized', async () => { const getForegroundProcess = vi .fn() .mockResolvedValueOnce('node') .mockResolvedValueOnce('codex') const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess }) syncSinglePty(runtime, 'pty-1', { paneTitle: 'bash' }) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(true) expect(getForegroundProcess).toHaveBeenCalledTimes(2) }) it('keeps Claude management titles suppressed after wrapper foreground refreshes', async () => { const getForegroundProcess = vi .fn() .mockResolvedValueOnce('node') .mockResolvedValueOnce('claude') const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'claude agents' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) expect(getForegroundProcess).toHaveBeenCalledTimes(2) }) it('lets adopted Claude agents pane titles use non-Claude foreground fallback', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'claude agents' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('keeps ready prompt evidence when an adopted pane title is neutral', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', title: 'Codex working' }) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'bash' }) runtime.onPtyData( 'pty-bg', ['OpenAI Codex', 'Model: gpt-5.4', 'Directory: /tmp/worktree-a'].join('\n'), 100 ) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('lets adopted pane agent titles override stale PTY Claude agents titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) runtime.onPtyData('pty-bg', '\x1b]0;claude agents\x07', 100) syncSinglePty(runtime, 'pty-bg', { paneTitle: 'claude working' }) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('lets current Claude agents PTY titles override stale runtime-created OSC titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) const pty = ( runtime as unknown as { ptysById: Map< string, { lastOscTitle: string | null lastOscTitleAt: number | null } > } ).ptysById.get('pty-bg') expect(pty).toBeDefined() if (!pty) { throw new Error('expected runtime PTY record') } pty.lastOscTitle = 'claude working' pty.lastOscTitleAt = 0 await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('does not let stale Claude agents OSC titles suppress current PTY title activity', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude', title: 'claude working' }) const pty = ( runtime as unknown as { ptysById: Map< string, { lastOscTitle: string | null lastOscTitleAt: number | null } > } ).ptysById.get('pty-bg') expect(pty).toBeDefined() if (!pty) { throw new Error('expected runtime PTY record') } pty.lastOscTitle = 'claude agents' pty.lastOscTitleAt = 0 await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('recognizes fresh runtime-created agent OSC titles over stale Claude agents launch titles', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) runtime.onPtyData('pty-bg', '\x1b]0;claude working\x07', 100) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('keeps Claude agents management evidence when controller refresh reports a Claude process title', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude', listProcesses: async () => [{ id: 'pty-bg', cwd: TEST_WORKTREE_PATH, title: 'claude' }] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) await runtime.getWorktreePs() await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('allows non-Claude foreground agents after preserved Claude agents management evidence', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'codex', listProcesses: async () => [{ id: 'pty-bg', cwd: TEST_WORKTREE_PATH, title: 'zsh' }] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) await runtime.getWorktreePs() await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('does not let stale PTY status override a fresh neutral PTY title', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude', title: 'Claude working' }) runtime.onPtyData('pty-bg', '\x1b]0;Claude working\x07', 100) runtime.onPtyData('pty-bg', '\x1b]0;zsh\x07', 101) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('does not use stale runtime-created PTY status when a neutral PTY title exists', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude', title: 'zsh' }) const pty = ( runtime as unknown as { ptysById: Map< string, { lastAgentStatus: 'working' | null } > } ).ptysById.get('pty-bg') expect(pty).toBeDefined() if (!pty) { throw new Error('expected runtime PTY record') } pty.lastAgentStatus = 'working' runtime.setPtyController(null) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('recognizes ready prompt evidence even with a stale Claude agents title', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'claude agents', title: 'claude agents' }) runtime.onPtyData( 'pty-bg', ['OpenAI Codex', 'Model: gpt-5.4', 'Directory: /tmp/worktree-a'].join('\n'), 100 ) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('recognizes runtime-created Codex PTY handles from the ready prompt', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex', title: 'worker' }) runtime.onPtyData( 'pty-bg', ['OpenAI Codex', 'Model: gpt-5.4', 'Directory: /tmp/worktree-a'].join('\n'), 100 ) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('recognizes runtime-created Antigravity PTY handles from the ready prompt', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'agy', title: 'worker' }) runtime.onPtyData('pty-bg', antigravityReadyScreen(), 100) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('recognizes Antigravity ready tails with the prompt before the model line', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'agy', title: 'worker' }) runtime.onPtyData('pty-bg', antigravityPromptBeforeModelReadyScreen(), 100) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('recognizes live leaf Antigravity terminals from the ready prompt', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: null } ] }) runtime.onPtyData('pty-1', antigravityReadyScreen(), 100) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(true) }) it('does not recognize partial Antigravity startup output as an agent', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'agy', title: 'worker' }) runtime.onPtyData( 'pty-bg', [ 'Antigravity CLI 1.0.3', 'user@example.com (Antigravity Business)', 'Gemini 3.5 Flash (High)' ].join('\n'), 100 ) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('rejects a later Antigravity header with a prompt but no model line', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'agy', title: 'worker' }) runtime.onPtyData( 'pty-bg', [ antigravityReadyScreen(), '\nAntigravity CLI 1.0.4\n', 'user@example.com (Antigravity Business)\n', '~/orca/workspaces/orca/agy-dispatch-issue\n', '>\n' ].join(''), 100 ) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('uses the latest Antigravity header when checking readiness', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'agy', title: 'worker' }) runtime.onPtyData( 'pty-bg', [ antigravityReadyScreen(), '\nAntigravity CLI 1.0.4\n', 'user@example.com (Antigravity Business)\n', 'Gemini 4 Experimental (High)\n', 'Do you trust this workspace directory?\n' ].join(''), 100 ) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(false) }) it('recognizes Antigravity prompts written as the current partial line', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'agy', title: 'worker' }) runtime.onPtyData( 'pty-bg', [ 'Antigravity CLI 1.0.3\n', 'user@example.com (Antigravity Business)\n', 'Gemini 3.5 Flash (High)\n', '~/orca/workspaces/orca/agy-dispatch-issue\n' ].join(''), 100 ) runtime.onPtyData('pty-bg', ' > ', 101) await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) }) it('does not classify agy workspace paths or titles without the ready prompt', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: '/tmp/agy-workspace', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: '/tmp/agy-workspace' } ] }) runtime.onPtyData('pty-1', 'cd /tmp/agy-workspace\n', 100) const [terminal] = (await runtime.listTerminals()).terminals await expect(runtime.isTerminalRunningAgent(terminal.handle)).resolves.toBe(false) }) it('keeps mobile terminal surfaces visible while their leaf handle is pending', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', status: 'pending-handle', terminal: null }) ]) }) it('keeps mobile terminal surfaces pending while a live leaf has no PTY', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: null, paneTitle: 'Terminal 1' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: 'tab-1::pane:1', status: 'pending-handle', terminal: null }) ]) }) it('selects a visible active pane when terminal visual layout prunes a stale leaf', async () => { const runtime = new OrcaRuntimeService(store) const parentLayout: TerminalLayoutSnapshot = { root: { type: 'split', direction: 'vertical', first: { type: 'leaf', leafId: 'pane:1' }, second: { type: 'leaf', leafId: 'pane:2' } }, activeLeafId: 'pane:1', expandedLeafId: null, ptyIdsByLeafId: { 'pane:2': 'pty-2' } } runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Split terminal', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:2', paneRuntimeId: 2, ptyId: 'pty-2', paneTitle: 'Live pane' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: null, activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Stale pane', parentLayout, isActive: true }, { type: 'terminal', id: 'tab-1::pane:2', parentTabId: 'tab-1', leafId: 'pane:2', title: 'Live pane', parentLayout, isActive: false } ] } ] }) const result = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) expect(result.visualLayouts).toMatchObject([ { worktreeId: TEST_WORKTREE_ID, root: { type: 'group', tabs: [ { tabId: 'tab-1', activeLeafId: 'pane:2', panes: { type: 'terminal', leafId: 'pane:2', active: true } } ] } } ]) }) it('omits stale browser session tabs that no longer have live webContents', async () => { const runtime = new OrcaRuntimeService(store) const tabList = vi.fn(() => ({ tabs: [ { browserPageId: 'browser-page-live', index: 0, url: 'https://live.example/', title: 'Live Browser', active: true } ] })) runtime.setAgentBrowserBridge({ tabList } as never) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'browser-unified-stale', activeTabType: 'browser', tabs: [ { type: 'browser', id: 'browser-unified-stale', title: 'Dead Browser', browserWorkspaceId: 'browser-workspace-stale', browserPageId: 'browser-page-stale', url: 'about:blank', loading: false, canGoBack: false, canGoForward: false, isActive: true }, { type: 'browser', id: 'browser-unified-live', title: 'Stale Title', browserWorkspaceId: 'browser-workspace-live', browserPageId: 'browser-page-live', url: 'https://stale.example/', loading: false, canGoBack: false, canGoForward: false, isActive: false } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(tabList).toHaveBeenCalledWith(TEST_WORKTREE_ID) expect(result.tabs).toEqual([ expect.objectContaining({ type: 'browser', id: 'browser-unified-live', browserPageId: 'browser-page-live', url: 'https://live.example/', title: 'Live Browser', isActive: true }) ]) expect(result.activeTabId).toBe('browser-unified-live') expect(result.activeTabType).toBe('browser') }) it('does not let the active browser webContents steal session focus from terminals', async () => { const runtime = new OrcaRuntimeService(store) const tabList = vi.fn(() => ({ tabs: [ { browserPageId: 'browser-page-1', index: 0, url: 'https://example.com/', title: 'Live Browser', active: true } ] })) runtime.setAgentBrowserBridge({ tabList } as never) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'terminal-tab::pane:1', activeTabType: 'terminal', tabs: [ { type: 'browser', id: 'browser-unified-1', title: 'Stale Browser', browserWorkspaceId: 'browser-workspace-1', browserPageId: 'browser-page-1', url: 'https://stale.example/', loading: false, canGoBack: false, canGoForward: false, isActive: false }, { type: 'terminal', id: 'terminal-tab::pane:1', parentTabId: 'terminal-tab', leafId: 'pane:1', title: 'Terminal 2', isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.activeTabId).toBe('terminal-tab::pane:1') expect(result.activeTabType).toBe('terminal') expect(result.tabs).toEqual([ expect.objectContaining({ type: 'browser', id: 'browser-unified-1', isActive: false, title: 'Live Browser' }), expect.objectContaining({ type: 'terminal', id: 'terminal-tab::pane:1', isActive: true }) ]) }) it('publishes terminal surface agent status for paired web clients', async () => { const runtime = new OrcaRuntimeService(store) const leafId = '11111111-1111-4111-8111-111111111111' const hostPaneKey = `tab-1:${leafId}` runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `tab-1::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${leafId}`, parentTabId: 'tab-1', leafId, title: 'codex [working]', agentStatus: { state: 'working', prompt: 'fix parity', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'codex', paneKey: hostPaneKey, terminalTitle: 'codex [working]', stateHistory: [] }, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: `tab-1::${leafId}`, status: 'pending-handle', terminal: null, agentStatus: expect.objectContaining({ state: 'working', prompt: 'fix parity', agentType: 'codex', paneKey: hostPaneKey }) }) ]) }) it('preserves authoritative OMP identity for Pi-compatible remote terminal snapshots', async () => { const runtime = new OrcaRuntimeService(store) const leafId = '11111111-1111-4111-8111-111111111111' const hostPaneKey = `tab-1:${leafId}` runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `tab-1::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${leafId}`, parentTabId: 'tab-1', leafId, title: '\u280b Pi', launchAgent: 'omp', agentStatus: { state: 'working', prompt: 'fix parity', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'pi', paneKey: hostPaneKey, terminalTitle: '\u280b Pi', stateHistory: [] }, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: '\u280b OMP', launchAgent: 'omp', agentStatus: expect.objectContaining({ state: 'working', agentType: 'omp', paneKey: hostPaneKey, terminalTitle: '\u280b OMP' }) }) ) }) it('derives remote OMP owner from live PTY metadata when the tab snapshot omits it', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-omp' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'omp', launchAgent: 'omp', title: 'OMP', activate: true }) const spawnCall = spawn.mock.calls[0]?.[0] expect(spawnCall).toEqual( expect.objectContaining({ tabId: expect.any(String), leafId: expect.any(String) }) ) const { tabId, leafId } = spawnCall as { tabId: string; leafId: string } runtime.syncWindowGraph(1, { tabs: [ { tabId, worktreeId: TEST_WORKTREE_ID, title: '\u280b π - tmp', activeLeafId: leafId, layout: null } ], leaves: [ { tabId, worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-omp', paneTitle: '\u280b π - tmp' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: null, activeTabId: `${tabId}::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `${tabId}::${leafId}`, parentTabId: tabId, leafId, ptyId: 'pty-omp', title: '\u280b π - tmp', isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: '\u280b OMP', launchAgent: 'omp' }) ) }) it('skips the foreground-process probe when the PTY launch agent is already known', async () => { // Why: foregroundAgent is only a fallback when launchAgent is unknown, so probing a launched agent burns a relay round-trip without changing the resolved owner. const getForegroundProcess = vi.fn(async () => 'omp') const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-omp' }), write: () => true, kill: () => true, getForegroundProcess }) runtime.attachWindow(1) await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'omp', launchAgent: 'omp', title: 'OMP', activate: true }) runtime.onPtyData('pty-omp', '\x1b]0;⠋ OMP\x07working\n', 100) runtime.onPtyData('pty-omp', '\x1b]0;OMP ready\x07idle\n', 200) expect(getForegroundProcess).not.toHaveBeenCalled() }) it('probes the foreground process only on a status transition for unknown launch agents', async () => { const getForegroundProcess = vi.fn(async () => 'omp') const runtime = createRuntime() runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess }) syncSinglePty(runtime, 'pty-bg') // Why: each probe dedups while in-flight; settle it before the next frame to prove the gate, not the dedup, suppresses extra probes. const settleProbe = (): Promise => new Promise((resolve) => setImmediate(resolve)) // Two working frames (spinner churn) collapse to a single status transition. runtime.onPtyData('pty-bg', '\x1b]0;⠋ OMP\x07alpha\n', 100) runtime.onPtyData('pty-bg', '\x1b]0;⠊ OMP\x07bravo\n', 200) await settleProbe() expect(getForegroundProcess).toHaveBeenCalledTimes(1) // Transition to idle is a second distinct status, so it probes again. runtime.onPtyData('pty-bg', '\x1b]0;OMP ready\x07charlie\n', 300) await settleProbe() expect(getForegroundProcess).toHaveBeenCalledTimes(2) // A repeated idle frame is not a transition, so it does not probe again. runtime.onPtyData('pty-bg', '\x1b]0;OMP ready\x07delta\n', 400) await settleProbe() expect(getForegroundProcess).toHaveBeenCalledTimes(2) }) it('normalizes Pi-compatible mobile session status to OMP for an unknown-launch foreground omp PTY', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-typed-omp' }) const getForegroundProcess = vi.fn(async () => 'omp') const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess }) const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'typed-omp-tab', leafId: HEADLESS_LEAF_ID, title: 'Terminal' }) runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi ready\x07', 123) await new Promise((resolve) => setImmediate(resolve)) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(getForegroundProcess).toHaveBeenCalledWith('pty-typed-omp') expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'OMP ready', agentStatus: expect.objectContaining({ state: 'done', agentType: 'omp', terminalHandle: terminal.handle, terminalTitle: 'OMP ready' }) }) ) expect(result.tabs[0]).not.toHaveProperty('launchAgent') }) it('waits for unknown-launch foreground owner before publishing Pi-compatible mobile status', async () => { const foregroundProcess = deferred() const spawn = vi.fn().mockResolvedValue({ id: 'pty-typed-omp' }) const getForegroundProcess = vi.fn(() => foregroundProcess.promise) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess }) const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'typed-omp-tab', leafId: HEADLESS_LEAF_ID, title: 'Terminal' }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi ready\x07', 123) await new Promise((resolve) => setImmediate(resolve)) expect(getForegroundProcess).toHaveBeenCalledWith('pty-typed-omp') expect(events).toHaveLength(0) foregroundProcess.resolve('omp') await new Promise((resolve) => setImmediate(resolve)) await waitForMobileSessionTabsEvents(events, 1) expect(events).toEqual([ expect.objectContaining({ tabs: [ expect.objectContaining({ type: 'terminal', title: 'OMP ready', agentStatus: expect.objectContaining({ state: 'done', agentType: 'omp', terminalHandle: terminal.handle, terminalTitle: 'OMP ready' }) }) ] }) ]) unsubscribe() }) it('keeps same-status Pi-compatible title changes queued behind the foreground owner probe', async () => { const foregroundProcess = deferred() const spawn = vi.fn().mockResolvedValue({ id: 'pty-typed-omp' }) const getForegroundProcess = vi.fn(() => foregroundProcess.promise) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess }) const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'typed-omp-tab', leafId: HEADLESS_LEAF_ID, title: 'Terminal' }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi ready\x07', 123) await new Promise((resolve) => setImmediate(resolve)) runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi idle\x07', 124) await new Promise((resolve) => setImmediate(resolve)) expect(getForegroundProcess).toHaveBeenCalledTimes(1) expect(events).toHaveLength(0) foregroundProcess.resolve('omp') await new Promise((resolve) => setImmediate(resolve)) await waitForMobileSessionTabsEvents(events, 1) expect(events).toEqual([ expect.objectContaining({ tabs: [ expect.objectContaining({ type: 'terminal', title: 'OMP ready', agentStatus: expect.objectContaining({ state: 'done', agentType: 'omp', terminalHandle: terminal.handle, terminalTitle: 'OMP ready' }) }) ] }) ]) unsubscribe() }) it('coalesces same-status title frames behind one post-title foreground probe', async () => { const staleForegroundProcess = deferred() const freshForegroundProcess = deferred() const spawn = vi.fn().mockResolvedValue({ id: 'pty-typed-omp' }) const getForegroundProcess = vi .fn() .mockReturnValueOnce(staleForegroundProcess.promise) .mockReturnValueOnce(freshForegroundProcess.promise) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess }) const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'typed-omp-tab', leafId: HEADLESS_LEAF_ID, title: 'Terminal' }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi ready\x07', 123) await new Promise((resolve) => setImmediate(resolve)) runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi idle\x07', 124) runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi done\x07', 125) await new Promise((resolve) => setImmediate(resolve)) expect(getForegroundProcess).toHaveBeenCalledTimes(1) expect(events).toHaveLength(0) staleForegroundProcess.resolve(null) await new Promise((resolve) => setImmediate(resolve)) expect(getForegroundProcess).toHaveBeenCalledTimes(2) expect(events).toHaveLength(0) freshForegroundProcess.resolve('omp') await new Promise((resolve) => setImmediate(resolve)) await waitForMobileSessionTabsEvents(events, 1) expect(getForegroundProcess).toHaveBeenCalledTimes(2) expect(events).toEqual([ expect.objectContaining({ tabs: [ expect.objectContaining({ type: 'terminal', title: 'OMP ready', agentStatus: expect.objectContaining({ state: 'done', agentType: 'omp', terminalHandle: terminal.handle, terminalTitle: 'OMP ready' }) }) ] }) ]) unsubscribe() }) it('starts a post-title foreground probe when an older pending probe finds no owner', async () => { const staleForegroundProcess = deferred() const freshForegroundProcess = deferred() const spawn = vi.fn().mockResolvedValue({ id: 'pty-typed-omp' }) const getForegroundProcess = vi .fn() .mockReturnValueOnce(staleForegroundProcess.promise) .mockReturnValueOnce(freshForegroundProcess.promise) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess }) const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'typed-omp-tab', leafId: HEADLESS_LEAF_ID, title: 'Terminal' }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) ;( runtime as unknown as { refreshPtyForegroundAgentFromController: (ptyId: string) => Promise } ).refreshPtyForegroundAgentFromController('pty-typed-omp') runtime.onPtyData('pty-typed-omp', '\x1b]0;Pi ready\x07', 123) await new Promise((resolve) => setImmediate(resolve)) expect(getForegroundProcess).toHaveBeenCalledTimes(1) expect(events).toHaveLength(0) staleForegroundProcess.resolve(null) await new Promise((resolve) => setImmediate(resolve)) expect(getForegroundProcess).toHaveBeenCalledTimes(2) expect(events).toHaveLength(0) freshForegroundProcess.resolve('omp') await new Promise((resolve) => setImmediate(resolve)) await waitForMobileSessionTabsEvents(events, 1) expect(events).toEqual([ expect.objectContaining({ tabs: [ expect.objectContaining({ type: 'terminal', title: 'OMP ready', agentStatus: expect.objectContaining({ state: 'done', agentType: 'omp', terminalHandle: terminal.handle, terminalTitle: 'OMP ready' }) }) ] }) ]) unsubscribe() }) it('keeps Pi-compatible mobile session status as Pi for an unknown-launch foreground pi PTY', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-typed-pi' }) const getForegroundProcess = vi.fn(async () => 'pi') const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess }) const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'typed-pi-tab', leafId: HEADLESS_LEAF_ID, title: 'Terminal' }) runtime.onPtyData('pty-typed-pi', '\x1b]0;Pi ready\x07', 123) await new Promise((resolve) => setImmediate(resolve)) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(getForegroundProcess).toHaveBeenCalledWith('pty-typed-pi') expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'Pi ready', agentStatus: expect.objectContaining({ state: 'done', agentType: 'pi', terminalHandle: terminal.handle, terminalTitle: 'Pi ready' }) }) ) expect(result.tabs[0]).not.toHaveProperty('launchAgent') }) it('keeps renderer-vetted mobile agent status for custom-titled terminals', async () => { const runtime = new OrcaRuntimeService(store) const leafId = '11111111-1111-4111-8111-111111111111' const hostPaneKey = `tab-1:${leafId}` runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: null, activeTabId: `tab-1::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${leafId}`, parentTabId: 'tab-1', leafId, title: 'claude agents', agentStatus: { state: 'working', prompt: 'fix parity', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'codex', paneKey: hostPaneKey, terminalTitle: 'codex [working]', stateHistory: [] }, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'claude agents', agentStatus: expect.objectContaining({ state: 'working', agentType: 'codex', paneKey: hostPaneKey }) }) ) }) it('suppresses saved mobile agent status when live evidence is the Claude agents screen', async () => { const runtime = new OrcaRuntimeService(store) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'claude working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'claude agents' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: null, activeTabId: `tab-1::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${leafId}`, parentTabId: 'tab-1', leafId, title: 'claude agents', agentStatus: { state: 'working', prompt: 'stale task', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'claude', paneKey: `tab-1:${leafId}`, terminalTitle: 'claude working', stateHistory: [] }, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'claude agents' }) ) // Stale "working" status is suppressed (no spinner), but agent identity is retained so native chat can still address the idle agent's transcript. const suppressed = result.tabs[0] expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.state).toBe('done') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.agentType).toBe('claude') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.terminalTitle).toBeUndefined() }) it('suppresses saved mobile agent status when the current terminal title is neutral', async () => { const runtime = new OrcaRuntimeService(store) const leafId = '11111111-1111-4111-8111-111111111111' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'claude working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'bash' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: null, activeTabId: `tab-1::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${leafId}`, parentTabId: 'tab-1', leafId, title: 'bash', agentStatus: { state: 'working', prompt: 'stale task', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'claude', paneKey: `tab-1:${leafId}`, terminalTitle: 'claude working', stateHistory: [] }, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'bash' }) ) // Stale "working" suppressed; agent identity retained for native chat. const suppressed = result.tabs[0] expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.state).toBe('done') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.agentType).toBe('claude') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.terminalTitle).toBeUndefined() }) it('suppresses saved mobile agent status when fresh live OSC title is Claude agents', async () => { const runtime = new OrcaRuntimeService(store) const leafId = '11111111-1111-4111-8111-111111111111' runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'claude working', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'claude working' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: null, activeTabId: `tab-1::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${leafId}`, parentTabId: 'tab-1', leafId, title: 'claude working', agentStatus: { state: 'working', prompt: 'stale task', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'claude', paneKey: `tab-1:${leafId}`, terminalTitle: 'claude working', stateHistory: [] }, isActive: true } ] } ] }) runtime.onPtyData('pty-1', '\x1b]0;claude agents\x07', 100) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'claude agents' }) ) // Stale "working" suppressed; agent identity retained for native chat. const suppressed = result.tabs[0] expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.state).toBe('done') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.agentType).toBe('claude') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.terminalTitle).toBeUndefined() }) it('keeps saved PTY bindings pending until the runtime knows the PTY is connected', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', ptyId: 'daemon-pty-1', parentLayout: { root: { type: 'leaf', leafId: 'pane:1' }, activeLeafId: 'pane:1', expandedLeafId: null, ptyIdsByLeafId: { 'pane:1': 'daemon-pty-1' } }, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: 'tab-1::pane:1', ptyId: 'daemon-pty-1', parentTabId: 'tab-1', leafId: 'pane:1', status: 'pending-handle', terminal: null }) ]) }) it('refreshes daemon PTY liveness before publishing mobile session tabs', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'daemon-pty-1', cwd: TEST_WORKTREE_PATH, title: 'daemon shell' } ] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', ptyId: 'daemon-pty-1', parentLayout: { root: { type: 'leaf', leafId: 'pane:1' }, activeLeafId: 'pane:1', expandedLeafId: null, ptyIdsByLeafId: { 'pane:1': 'daemon-pty-1' } }, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: 'tab-1::pane:1', ptyId: 'daemon-pty-1', status: 'ready', terminal: expect.stringMatching(/^term_/) }) ]) }) it('does not invalidate a newly spawned SSH pane from an overlapping stale process list', async () => { const runtime = new OrcaRuntimeService(store) const ptyId = 'ssh:ssh-1@@pty-new' runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [], hasPty: (candidate) => candidate === ptyId }) runtime.registerPty(ptyId, TEST_WORKTREE_ID, 'ssh-1', { tabId: 'tab-1', leafId: HEADLESS_LEAF_ID }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'ssh-spawn-list-race', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `tab-1::${HEADLESS_LEAF_ID}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${HEADLESS_LEAF_ID}`, parentTabId: 'tab-1', leafId: HEADLESS_LEAF_ID, title: 'SSH terminal', ptyId, isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toEqual([ expect.objectContaining({ ptyId, status: 'ready', terminal: expect.any(String) }) ]) }) it('reattaches mobile terminal surfaces from saved PTY bindings when the PTY is connected', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'daemon-pty-1' } ] }) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', ptyId: 'daemon-pty-1', isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: 'tab-1::pane:1', status: 'ready', terminal: expect.stringMatching(/^term_/) }) ]) expect(runtime.resolveLeafForHandle((result.tabs[0] as { terminal: string }).terminal)).toEqual( { ptyId: 'daemon-pty-1' } ) }) it('retires exited saved PTY bindings instead of publishing a pending ghost', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', ptyId: 'daemon-pty-1', isActive: true } ] } ] }) await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) runtime.onPtyExit('daemon-pty-1', 0) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result).toMatchObject({ activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] }) }) it('resolves mobile terminal surfaces by exact split leaf', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: 'pane:2', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'left' }, { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:2', paneRuntimeId: 2, ptyId: 'pty-2', paneTitle: 'right' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:2', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', isActive: false }, { type: 'terminal', id: 'tab-1::pane:2', parentTabId: 'tab-1', leafId: 'pane:2', title: 'Terminal 1', isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs).toHaveLength(2) expect(result.tabs).toEqual([ expect.objectContaining({ id: 'tab-1::pane:1', title: 'left', status: 'ready' }), expect.objectContaining({ id: 'tab-1::pane:2', title: 'right', status: 'ready' }) ]) const [left, right] = result.tabs expect(left?.type).toBe('terminal') expect(right?.type).toBe('terminal') if (left?.type === 'terminal' && right?.type === 'terminal') { expect(left.terminal).not.toBe(right.terminal) } }) it('keeps published mobile terminal handles usable across renderer graph epochs', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'Terminal 1' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', isActive: true } ] } ] }) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const tab = result.tabs[0] expect(tab?.type).toBe('terminal') if (tab?.type !== 'terminal' || tab.status !== 'ready') { throw new Error('expected ready terminal tab') } runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'Terminal 1' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-2', snapshotVersion: 2, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', isActive: true } ] } ] }) runtime.onPtyData('pty-1', 'after graph sync\n', 100) await expect(runtime.readTerminal(tab.terminal)).resolves.toMatchObject({ handle: tab.terminal, tail: ['after graph sync'] }) }) it('closes the matching mobile terminal UUID leaf without closing the whole tab', async () => { const closeTerminal = vi.fn() const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn(), write: () => true, kill, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal, sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) const leftLeafId = '11111111-1111-4111-8111-111111111111' const rightLeafId = '22222222-2222-4222-8222-222222222222' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: rightLeafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: leftLeafId, paneRuntimeId: 1, ptyId: 'pty-left', paneTitle: 'left' }, { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: rightLeafId, paneRuntimeId: 2, ptyId: 'pty-right', paneTitle: 'right' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `tab-1::${rightLeafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-1::${rightLeafId}`, parentTabId: 'tab-1', leafId: rightLeafId, title: 'right', isActive: true } ] } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, `tab-1::${rightLeafId}`) expect(kill).toHaveBeenCalledWith('pty-right') expect(closeTerminal).not.toHaveBeenCalled() }) it('closes the whole mobile terminal tab when addressed by parent tab id', async () => { const closeTerminal = vi.fn() const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn(), write: () => true, kill, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal, sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1', paneTitle: 'Terminal 1' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', isActive: true } ] } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'tab-1') expect(closeTerminal).toHaveBeenCalledWith('tab-1') expect(kill).not.toHaveBeenCalled() }) it('activates the active split leaf when addressed by parent tab id', async () => { const focusTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal, closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:2', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'left', isActive: false }, { type: 'terminal', id: 'tab-1::pane:2', parentTabId: 'tab-1', leafId: 'pane:2', title: 'right', isActive: true } ] } ] }) await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'tab-1') expect(focusTerminal).toHaveBeenCalledWith('tab-1', TEST_WORKTREE_ID, 'pane:2') }) it('activates mobile session tabs without focusing desktop clients when requested', async () => { const focusTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal, closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:2', activeTabType: 'terminal', tabGroups: [{ id: 'group-1', activeTabId: 'tab-1', tabOrder: ['tab-1'] }], tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', ptyId: 'pty-pane-1', title: 'left', isActive: false }, { type: 'terminal', id: 'tab-1::pane:2', parentTabId: 'tab-1', leafId: 'pane:2', ptyId: 'pty-pane-2', title: 'right', isActive: true } ] } ] }) runtime.registerPty('pty-pane-1', TEST_WORKTREE_ID) runtime.registerPty('pty-pane-2', TEST_WORKTREE_ID) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, 'tab-1::pane:1', undefined, { notifyClients: false } ) expect(focusTerminal).not.toHaveBeenCalled() expect(activated).toMatchObject({ activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabGroups: [expect.objectContaining({ id: 'group-1', activeTabId: 'tab-1' })] }) expect(activated.tabs).toEqual([ expect.objectContaining({ id: 'tab-1::pane:1', isActive: true }), expect.objectContaining({ id: 'tab-1::pane:2', isActive: false }) ]) }) it('clears unread metadata on mobile worktree activation without focusing desktop clients', async () => { const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: true }) } const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }) const activateWorktree = vi.fn() const worktreesChanged = vi.fn() const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta } as never) runtime.setNotifier({ worktreesChanged, reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false }) expect(setWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, { isUnread: false }) expect(metaById[TEST_WORKTREE_ID]?.isUnread).toBe(false) expect(worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID) expect(activateWorktree).not.toHaveBeenCalled() }) it('wakes slept agents on the host renderer when a phone activates a worktree', async () => { // Seed isUnread:false so the unread-clear branch stays quiet, isolating the mobile slept-agent wake. const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: false }) } const activateWorktree = vi.fn() const resumeSleepingAgents = vi.fn() const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], getWorkspaceSession: () => ({ ...getDefaultWorkspaceSession(), sleepingAgentSessionsByPaneKey: { 'tab-1:leaf-1': { paneKey: 'tab-1:leaf-1', tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, agent: 'codex', providerSession: { key: 'session_id', id: 'session-1' }, prompt: 'test', state: 'done', capturedAt: 1, updatedAt: 1, origin: 'worktree-sleep' } } }) } as never) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), resumeSleepingAgents, terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) // A renderer must be attached to receive the wake; headless serve reports 'unsupported-headless' instead. electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) const result = await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false, clientKind: 'mobile' }) // INV-2: mobile wake never navigates the desktop (no activateWorktree); it routes through the renderer's navigation-free wake. expect(resumeSleepingAgents).toHaveBeenCalledWith(TEST_WORKTREE_ID) expect(activateWorktree).not.toHaveBeenCalled() expect(result.sleepingAgentWake).toBe('requested') }) it('reports the wake as unsupported when a phone activates a worktree on headless serve', async () => { // Why: without a renderer nothing holds the sleeping records so nothing wakes; the result must say so or the phone shows slept agents as resumed (#7906). const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: false }) } const resumeSleepingAgents = vi.fn() const getWorkspaceSession = vi.fn(() => ({ ...getDefaultWorkspaceSession(), sleepingAgentSessionsByPaneKey: { 'tab-1:leaf-1': { paneKey: 'tab-1:leaf-1', tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, agent: 'codex', providerSession: { key: 'session_id', id: 'session-1' }, prompt: 'test', state: 'done', capturedAt: 1, updatedAt: 1, origin: 'worktree-sleep' } } })) const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], getWorkspaceSession } as never) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), resumeSleepingAgents, terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) // Headless: graph ready but no BrowserWindow backs the authoritative id, so getAvailableAuthoritativeWindow() is null. electronMocks.BrowserWindow.fromId.mockReturnValue(null as never) runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) const result = await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false, clientKind: 'mobile' }) expect(result.activated).toBe(true) expect(result.sleepingAgentWake).toBe('unsupported-headless') // Why: sleeping records are host-partitioned; the check must read the repo's execution host partition, not always the local one. expect(getWorkspaceSession).toHaveBeenCalledWith('local') expect(resumeSleepingAgents).not.toHaveBeenCalled() }) it('does not report headless wake degradation without sleeping records', async () => { const runtime = new OrcaRuntimeService(store as never) electronMocks.BrowserWindow.fromId.mockReturnValue(null as never) runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) const result = await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false, clientKind: 'mobile' }) expect(result.sleepingAgentWake).toBe('not-applicable') }) it('does not wake slept agents for non-mobile session-only activation', async () => { const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: false }) } const resumeSleepingAgents = vi.fn() const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId] } as never) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), resumeSleepingAgents, terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) // INV-3: web/desktop runtime clients keep their existing wake-on-activation paths; the renderer notifier wake is mobile-scoped. const result = await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false, clientKind: 'runtime' }) expect(resumeSleepingAgents).not.toHaveBeenCalled() expect(result.sleepingAgentWake).toBe('not-applicable') }) it('does not rewrite unread metadata when a mobile activation finds the worktree already read', async () => { // Why: seed instanceId so worktree resolution doesn't emit its own metadata-stamp write, isolating the assertion to the unread clear. const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: false, instanceId: 'wt-instance' }) } const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }) const worktreesChanged = vi.fn() const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta } as never) runtime.setNotifier({ worktreesChanged, reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false }) expect(setWorktreeMeta).not.toHaveBeenCalled() expect(worktreesChanged).not.toHaveBeenCalled() metaById[TEST_WORKTREE_ID] = makeWorktreeMeta({ isUnread: true, instanceId: 'wt-instance' }) setWorktreeMeta.mockClear() worktreesChanged.mockClear() await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false }) await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false }) expect(setWorktreeMeta).toHaveBeenCalledTimes(1) expect(setWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, { isUnread: false }) expect(worktreesChanged).toHaveBeenCalledTimes(1) expect(worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID) }) it('returns unread:false from worktree.ps after a mobile activation clears the flag', async () => { const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: true }) } const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }) const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta } as never) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) const beforeActivation = await runtime.getWorktreePs() expect( beforeActivation.worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID) ?.unread ).toBe(true) await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false }) const afterActivation = await runtime.getWorktreePs() expect( afterActivation.worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID)?.unread ).toBe(false) }) it('materializes pending mobile session terminals without focusing desktop clients', async () => { const persistedPtyId = `${TEST_WORKTREE_ID}@@mobile-only-pty` const spawn = vi.fn().mockResolvedValue({ id: persistedPtyId }) const focusTerminal = vi.fn() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: persistedPtyId, worktreeId: TEST_WORKTREE_ID, title: 'Persisted Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: persistedPtyId }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal, closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, 'host-tab', HEADLESS_LEAF_ID, { notifyClients: false } ) expect(focusTerminal).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ worktreeId: TEST_WORKTREE_ID, tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, sessionId: persistedPtyId, persistHostSessionBinding: true }) ) expect(activated.tabs).toEqual([ expect.objectContaining({ id: `host-tab::${HEADLESS_LEAF_ID}`, isActive: true, status: 'ready', terminal: expect.any(String) }) ]) }) it('materializes phone-local pending terminal tabs without stored PTY bindings', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'fresh-mobile-pty' }) const focusTerminal = vi.fn() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: null, worktreeId: TEST_WORKTREE_ID, title: 'Persisted Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: undefined }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal, closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, 'host-tab', HEADLESS_LEAF_ID, { notifyClients: false } ) expect(focusTerminal).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ worktreeId: TEST_WORKTREE_ID, tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, sessionId: expect.stringMatching(/^serve-/), persistHostSessionBinding: true }) ) expect(activated.tabs).toEqual([ expect.objectContaining({ id: `host-tab::${HEADLESS_LEAF_ID}`, status: 'ready', terminal: expect.any(String) }) ]) }) it('keeps the target group active when phone-local activation materializes a tab', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'group-target-pty' }) const focusTerminal = vi.fn() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'host-tab' }, tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: null, worktreeId: TEST_WORKTREE_ID, title: 'Left', customTitle: null, color: null, sortOrder: 0, createdAt: 1 }, { id: 'host-tab-2', ptyId: null, worktreeId: TEST_WORKTREE_ID, title: 'Right', customTitle: null, color: null, sortOrder: 1, createdAt: 2 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: undefined }), 'host-tab-2': makeHeadlessTerminalLayout({ [HEADLESS_SECOND_LEAF_ID]: undefined }) }, tabGroups: { [TEST_WORKTREE_ID]: [ { id: 'group-left', worktreeId: TEST_WORKTREE_ID, activeTabId: 'host-tab', tabOrder: ['host-tab'] }, { id: 'group-right', worktreeId: TEST_WORKTREE_ID, activeTabId: 'host-tab-2', tabOrder: ['host-tab-2'] } ] } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal, closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, 'host-tab-2', HEADLESS_SECOND_LEAF_ID, { notifyClients: false } ) expect(focusTerminal).not.toHaveBeenCalled() expect(activated.activeGroupId).toBe('group-right') expect(activated.tabGroups).toEqual([ expect.objectContaining({ id: 'group-left', activeTabId: 'host-tab' }), expect.objectContaining({ id: 'group-right', activeTabId: 'host-tab-2' }) ]) expect(activated.activeTabId).toBe(`host-tab-2::${HEADLESS_SECOND_LEAF_ID}`) }) it('refreshes stale daemon liveness before phone-local terminal materialization', async () => { const stalePtyId = `${TEST_WORKTREE_ID}@@stale-mobile-pty` const spawn = vi.fn().mockResolvedValue({ id: stalePtyId }) const listProcesses = vi.fn(async () => []) const focusTerminal = vi.fn() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: stalePtyId, worktreeId: TEST_WORKTREE_ID, title: 'Persisted Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: stalePtyId }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.registerPty(stalePtyId, TEST_WORKTREE_ID) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal, closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses }) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, 'host-tab', HEADLESS_LEAF_ID, { notifyClients: false } ) expect(listProcesses).toHaveBeenCalled() expect(focusTerminal).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ sessionId: stalePtyId, tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }) ) expect(activated.tabs).toEqual([ expect.objectContaining({ id: `host-tab::${HEADLESS_LEAF_ID}`, status: 'ready', terminal: expect.any(String) }) ]) }) it('closes browser mobile session tabs when addressed by browser workspace id', async () => { const closeSessionTab = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), closeSessionTab, sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'browser-unified-1', activeTabType: 'browser', tabs: [ { type: 'browser', id: 'browser-unified-1', title: 'Browser', browserWorkspaceId: 'browser-workspace-1', browserPageId: 'browser-page-1', url: 'https://example.com/', loading: false, canGoBack: false, canGoForward: false, isActive: true } ] } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'browser-workspace-1') expect(closeSessionTab).toHaveBeenCalledWith('browser-unified-1', TEST_WORKTREE_ID) }) it('creates mobile session terminals in a headless runtime server', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-headless' }) const runtime = new OrcaRuntimeService(store) const persistViewMode = vi.spyOn( runtime as unknown as { persistHeadlessSessionTabProps: ( worktreeId: string, tabId: string, props: { viewMode: 'terminal' | 'chat' } ) => void }, 'persistHeadlessSessionTabProps' ) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { viewMode: 'chat' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, tabId: expect.stringMatching(UUID_RE), leafId: expect.stringMatching(UUID_RE), persistHostSessionBinding: true, preAllocatedHandle: expect.stringMatching(/^term_/) }) ) expect(result.tab).toMatchObject({ type: 'terminal', status: 'ready', terminal: expect.stringMatching(/^term_/), viewMode: 'chat', isActive: true }) expect(persistViewMode).toHaveBeenCalledWith(TEST_WORKTREE_ID, result.tab.parentTabId, { viewMode: 'chat' }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs).toEqual([ expect.objectContaining({ id: result.tab.id, status: 'ready', terminal: result.tab.terminal }) ]) }) it('selects a created terminal only for the paired caller', async () => { let spawnIndex = 0 const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: `pty-headless-${++spawnIndex}` })), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const hostTerminal = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`) const callerTerminal = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { clientNavigationId: 'device-a', navigation: 'caller' }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).activeTabId).toBe( hostTerminal.tab.id ) expect( (await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`, 'device-a')).activeTabId ).toBe(callerTerminal.tab.id) expect( (await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`, 'device-b')).activeTabId ).toBe(hostTerminal.tab.id) }) it('scopes terminal-create idempotency to the paired caller', async () => { let spawnIndex = 0 const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: `pty-headless-${++spawnIndex}` })), write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const [createdA, createdB] = await Promise.all([ runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { clientNavigationId: 'device-a', navigation: 'caller', clientMutationId: 'same-mutation' }), runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { clientNavigationId: 'device-b', navigation: 'caller', clientMutationId: 'same-mutation' }) ]) expect(createdA.tab.id).not.toBe(createdB.tab.id) expect(spawnIndex).toBe(2) }) it('creates mobile session terminals for folder workspaces in a headless runtime server', async () => { const folderPath = await mkdtemp(join(tmpdir(), 'orca-mobile-folder-workspace-')) const spawn = vi.fn().mockResolvedValue({ id: 'pty-mobile-folder' }) const folderWorkspace = makeFolderWorkspace({ folderPath }) const projectGroup = makeFolderProjectGroup({ parentPath: folderPath }) const runtime = new OrcaRuntimeService( createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never ) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const result = await runtime.createMobileSessionTerminal(`id:${TEST_FOLDER_WORKSPACE_KEY}`) const spawnCall = spawn.mock.calls[0]?.[0] as | { cwd?: string; env?: Record; worktreeId?: string } | undefined const spawnedEnv = spawnCall?.env ?? {} expect(spawnCall).toMatchObject({ cwd: folderPath, worktreeId: TEST_FOLDER_WORKSPACE_KEY, persistHostSessionBinding: true }) expectStablePaneKeyEnv(spawnedEnv) expect(spawnedEnv.ORCA_WORKSPACE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY) expect(spawnedEnv.ORCA_PROJECT_GROUP_ID).toBe(TEST_FOLDER_PROJECT_GROUP_ID) expect(spawnedEnv.ORCA_WORKSPACE_ROOT).toBe(folderPath) expect(result.tab).toMatchObject({ type: 'terminal', status: 'ready', terminal: expect.stringMatching(/^term_/), isActive: true }) }) it('spawns fresh headless SSH mobile session terminals instead of reattaching synthetic local ids', async () => { const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const remoteStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined) } const spawn = vi.fn().mockResolvedValue({ id: 'ssh:ssh-1@@remote-pty' }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ connectionId: 'ssh-1', worktreeId: TEST_WORKTREE_ID, persistHostSessionBinding: true }) ) expect(spawn.mock.calls[0]?.[0]).not.toHaveProperty('sessionId') }) it('hydrates headless mobile session terminals from the host workspace session', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: `host-tab::${HEADLESS_LEAF_ID}`, parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, ptyId: 'persisted-pty', status: 'pending-handle', terminal: null, isActive: true }) ]) expect(listed.tabGroups?.[0]).toMatchObject({ activeTabId: 'host-tab', tabOrder: ['host-tab'] }) }) it('hydrates an SSH worktree only from its SSH workspace-session partition', async () => { const localSession = makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'local-decoy-tab', ptyId: 'local-decoy-pty', worktreeId: TEST_WORKTREE_ID, title: 'Local decoy', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'local-decoy-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'local-decoy-pty' }) } }) const sshPtyId = 'ssh:ssh-1@@remote-pty' const sshSession = makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'ssh-host-tab', ptyId: sshPtyId, worktreeId: TEST_WORKTREE_ID, title: 'SSH host terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'ssh-host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: sshPtyId }) } }) const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const getWorkspaceSession = vi.fn((hostId?: string | null) => hostId === 'ssh:ssh-1' ? sshSession : localSession ) const runtime = new OrcaRuntimeService({ ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getWorkspaceSession } as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs).toEqual([ expect.objectContaining({ parentTabId: 'ssh-host-tab', ptyId: sshPtyId }) ]) expect(listed.tabs).not.toEqual([expect.objectContaining({ parentTabId: 'local-decoy-tab' })]) expect(getWorkspaceSession).toHaveBeenCalledWith('ssh:ssh-1') }) it('closes a headless SSH tab only in its SSH workspace-session partition', async () => { const sshPtyId = 'ssh:ssh-1@@remote-pty' const localSession = makeWorkspaceSessionWithHeadlessTerminal() let sshSession = makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'ssh-host-tab', ptyId: sshPtyId, worktreeId: TEST_WORKTREE_ID, title: 'SSH host terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'ssh-host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: sshPtyId }) } }) const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const setWorkspaceSession = vi.fn((session: WorkspaceSessionState, hostId?: string | null) => { expect(hostId).toBe('ssh:ssh-1') sshSession = session }) const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService({ ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getWorkspaceSession: (hostId?: string | null) => hostId === 'ssh:ssh-1' ? sshSession : localSession, setWorkspaceSession } as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'ssh-host-tab') expect(sshSession.tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(localSession.tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(setWorkspaceSession).toHaveBeenCalledTimes(1) expect(kill).toHaveBeenCalledWith(sshPtyId) }) it('keeps live headless mobile session terminals when a desktop renderer publishes without them', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'serve-mobile-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const created = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents: { send: vi.fn() } }) runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-empty', snapshotVersion: 1, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: created.tab.id, parentTabId: created.tab.parentTabId, leafId: created.tab.leafId, ptyId: 'serve-mobile-pty', status: 'ready' }) ]) }) it('publishes laptop-created remote runtime terminals to phone session tabs', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-empty', snapshotVersion: 1, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { command: "claude 'work on the issue'", tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) runtime.onPtyData('laptop-created-pty', '\x1b]0;Codex working\x07', Date.now()) runtime.onPtyData('laptop-created-pty', 'Claude is working...\r\n', Date.now()) const phoneTabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(laptopTerminal.surface).toBe('background') expect(phoneTabs.tabs).toEqual([ expect.objectContaining({ type: 'terminal', parentTabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID, status: 'ready', terminal: laptopTerminal.handle, agentStatus: expect.objectContaining({ state: 'working', paneKey: `laptop-tab:${HEADLESS_LEAF_ID}`, terminalHandle: laptopTerminal.handle }) }) ]) await expect(runtime.readTerminal(laptopTerminal.handle)).resolves.toMatchObject({ tail: ['Claude is working...'] }) }) it('keeps background-presentation PTY-backed mobile session tabs inactive', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true, presentation: 'background', tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) const phoneTabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(phoneTabs.activeTabId).toBeNull() expect(phoneTabs.tabs[0]).toMatchObject({ type: 'terminal', id: `laptop-tab::${HEADLESS_LEAF_ID}`, isActive: false }) }) it('replaces pending phone session tabs when a laptop-created remote PTY becomes live', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-pending', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `laptop-tab::${HEADLESS_LEAF_ID}`, activeTabType: 'terminal', tabGroups: [{ id: 'group-1', activeTabId: 'laptop-tab', tabOrder: ['laptop-tab'] }], tabs: [ { type: 'terminal', id: `laptop-tab::${HEADLESS_LEAF_ID}`, parentTabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID, title: 'Starting Claude', isActive: true } ] } ] }) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) const phoneTabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(phoneTabs.tabs).toHaveLength(1) expect(phoneTabs.tabs[0]).toMatchObject({ type: 'terminal', id: `laptop-tab::${HEADLESS_LEAF_ID}`, status: 'ready', terminal: laptopTerminal.handle }) }) it('publishes laptop-created remote runtime split terminals to phone session tabs', async () => { const spawn = vi .fn() .mockResolvedValueOnce({ id: 'laptop-created-pty' }) .mockResolvedValueOnce({ id: 'laptop-split-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) const split = await runtime.splitTerminal(laptopTerminal.handle, { direction: 'vertical' }) const phoneTabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const terminalTabs = phoneTabs.tabs.filter((tab) => tab.type === 'terminal') expect(split.tabId).toBe('laptop-tab') expect(terminalTabs).toHaveLength(2) expect(terminalTabs).toEqual( expect.arrayContaining([ expect.objectContaining({ parentTabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID, status: 'ready', terminal: laptopTerminal.handle }), expect.objectContaining({ parentTabId: 'laptop-tab', status: 'ready', terminal: split.handle }) ]) ) }) it('pushes PTY-backed mobile session tab title and agent status changes to subscribers', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) events.length = 0 runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07', 123) runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude waiting for permission\x07', 124) await waitForMobileSessionTabsEvents(events, 1) expect(events).toEqual([ expect.objectContaining({ tabs: [ expect.objectContaining({ type: 'terminal', agentStatus: expect.objectContaining({ state: 'blocked', terminalHandle: laptopTerminal.handle }) }) ] }) ]) unsubscribe() }) it('does not publish stale PTY-backed mobile agent status for Claude agents screens', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) const events: RuntimeMobileSessionTabsResult[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) events.length = 0 runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07', 123) runtime.onPtyData('laptop-created-pty', '\x1b]0;claude agents\x07', 124) await waitForMobileSessionTabsEvents(events, 1) expect(events[0]?.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'claude agents' }) ) expect(events[0]?.tabs[0]).not.toHaveProperty('agentStatus') unsubscribe() }) it('uses fresh PTY management titles over stale mobile snapshot and OSC titles', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => 'claude' }) const leafId = HEADLESS_LEAF_ID await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId }) runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07', 123) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'laptop-tab', worktreeId: TEST_WORKTREE_ID, title: 'Claude working', activeLeafId: leafId, layout: null } ], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-stale', snapshotVersion: 1, activeGroupId: null, activeTabId: `laptop-tab::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `laptop-tab::${leafId}`, parentTabId: 'laptop-tab', leafId, title: 'Claude working', agentStatus: { state: 'working', prompt: 'stale task', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'claude', paneKey: `laptop-tab:${leafId}`, terminalTitle: 'Claude working', stateHistory: [] }, isActive: true } ] } ] }) runtime.onPtyData('laptop-created-pty', '\x1b]0;claude agents\x07', 124) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'claude agents' }) ) // Stale "working" suppressed; agent identity retained for native chat. const suppressed = result.tabs[0] expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.state).toBe('done') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.agentType).toBe('claude') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.terminalTitle).toBeUndefined() }) it('uses fresh neutral PTY titles over stale mobile snapshot and OSC titles', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const leafId = HEADLESS_LEAF_ID await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId }) runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07', 123) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'laptop-tab', worktreeId: TEST_WORKTREE_ID, title: 'Claude working', activeLeafId: leafId, layout: null } ], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-stale', snapshotVersion: 1, activeGroupId: null, activeTabId: `laptop-tab::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `laptop-tab::${leafId}`, parentTabId: 'laptop-tab', leafId, title: 'Claude working', agentStatus: { state: 'working', prompt: 'stale task', updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000, agentType: 'claude', paneKey: `laptop-tab:${leafId}`, terminalTitle: 'Claude working', stateHistory: [] }, isActive: true } ] } ] }) runtime.onPtyData('laptop-created-pty', '\x1b]0;zsh\x07', 124) const result = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result.tabs[0]).toEqual( expect.objectContaining({ type: 'terminal', title: 'zsh' }) ) // Stale "working" suppressed; agent identity retained for native chat. const suppressed = result.tabs[0] expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.state).toBe('done') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.agentType).toBe('claude') expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.terminalTitle).toBeUndefined() }) it('pushes PTY-backed mobile session retirement when a server PTY exits', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const events: RuntimeMobileSessionTabsResult[] = [] runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) events.length = 0 runtime.onPtyExit('laptop-created-pty', 0) expect(events).toEqual([ expect.objectContaining({ activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] }) ]) await expect(runtime.readTerminal(laptopTerminal.handle)).resolves.toMatchObject({ status: 'exited' }) }) it('operates PTY-backed mobile session terminals without a renderer graph', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ closeTerminal } as never) runtime.setPtyController({ spawn, write: () => true, kill, getForegroundProcess: async () => null }) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) await expect(runtime.renameTerminal(laptopTerminal.handle, 'Shared Claude')).resolves.toEqual({ handle: laptopTerminal.handle, tabId: 'laptop-tab', title: 'Shared Claude' }) await expect(runtime.focusTerminal(laptopTerminal.handle)).resolves.toEqual({ handle: laptopTerminal.handle, tabId: 'laptop-tab', worktreeId: TEST_WORKTREE_ID }) await expect(runtime.closeTerminal(laptopTerminal.handle)).resolves.toEqual({ handle: laptopTerminal.handle, tabId: 'laptop-tab', ptyKilled: true }) expect(kill).toHaveBeenCalledWith('laptop-created-pty') expect(closeTerminal).toHaveBeenCalledWith('laptop-tab') }) it('waits for renderer acknowledgement before returning a whole-tab close receipt', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const acknowledged = makeDeferred() const closeTerminalTab = vi.fn(() => acknowledged.promise) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setNotifier({ closeTerminal: vi.fn(), closeTerminalTab } as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Durable', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: 'persisted-pty' } ] }) const [terminal] = (await runtime.listTerminals()).terminals const pending = runtime.closeTerminalTab(terminal.handle) let settled = false void pending.finally(() => { settled = true }) await vi.waitFor(() => expect(closeTerminalTab).toHaveBeenCalledWith('host-tab')) expect(settled).toBe(false) acknowledged.resolve() await expect(pending).resolves.toEqual({ handle: terminal.handle, tabId: 'host-tab', closeMode: 'tab', ptyKilled: false }) }) it('durably closes every split leaf without a renderer', async () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'durable-tab', ptyId: null, worktreeId: TEST_WORKTREE_ID, title: 'Durable', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'durable-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: undefined }) } }) ) const flushOrThrow = vi.fn() const spawn = vi .fn() .mockResolvedValueOnce({ id: 'headless-left' }) .mockResolvedValueOnce({ id: 'headless-right' }) const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService({ ...runtimeStore, flushOrThrow } as never) runtime.setPtyController({ spawn, write: () => true, kill, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'durable-tab', leafId: HEADLESS_LEAF_ID }) await runtime.splitTerminal(terminal.handle, { direction: 'vertical' }) await runtime.closeTerminalTab(terminal.handle) expect(kill).toHaveBeenCalledWith('headless-left') expect(kill).toHaveBeenCalledWith('headless-right') expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['durable-tab']).toBeUndefined() expect(flushOrThrow).toHaveBeenCalledTimes(1) }) it('lists PTY-backed mobile session terminals without a renderer graph', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID }) runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07hello\r\n', 123) await expect(runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)).resolves.toMatchObject({ terminals: [ expect.objectContaining({ handle: laptopTerminal.handle, worktreeId: TEST_WORKTREE_ID, title: 'Claude working', connected: true, preview: 'hello' }) ], totalCount: 1, truncated: false }) }) it('shows and resolves active PTY-backed mobile session terminals without a renderer graph', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID, activate: true }) runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07hello\r\n', 123) await expect(runtime.resolveActiveTerminal(`id:${TEST_WORKTREE_ID}`)).resolves.toBe( laptopTerminal.handle ) await expect(runtime.showTerminal(laptopTerminal.handle)).resolves.toMatchObject({ handle: laptopTerminal.handle, tabId: 'laptop-tab', leafId: HEADLESS_LEAF_ID, worktreeId: TEST_WORKTREE_ID, title: 'Claude working', connected: true, ptyId: 'laptop-created-pty' }) }) it('keeps split sibling headless mobile terminal leaves when a desktop renderer omits them', async () => { const runtime = new OrcaRuntimeService(store) runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'headless:split-siblings', snapshotVersion: 1, activeGroupId: 'headless-group', activeTabId: 'host-tab::pane:2', activeTabType: 'terminal', tabGroups: [ { id: 'headless-group', activeTabId: 'host-tab', tabOrder: ['host-tab'] } ], tabs: [ { type: 'terminal', id: 'host-tab::pane:1', parentTabId: 'host-tab', leafId: 'pane:1', title: 'left', isActive: false }, { type: 'terminal', id: 'host-tab::pane:2', parentTabId: 'host-tab', leafId: 'pane:2', title: 'right', isActive: true } ] } ] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-empty', snapshotVersion: 2, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: 'host-tab::pane:1', parentTabId: 'host-tab', leafId: 'pane:1' }), expect.objectContaining({ type: 'terminal', id: 'host-tab::pane:2', parentTabId: 'host-tab', leafId: 'pane:2' }) ]) expect(listed.activeTabId).toBe('host-tab::pane:2') }) it('keeps a headless tab-group split alive when a new tab is created', async () => { // Regression: drag-to-split-group was client-only and the headless host rejected it, coalescing groups on new-tab; the host must model + persist the split. let ptyCounter = 0 const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: `split-group-pty-${++ptyCounter}` })), write: () => true, kill: () => true, getForegroundProcess: async () => null }) const first = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const second = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const beforeSplit = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(beforeSplit.tabGroups).toHaveLength(1) const sourceGroupId = beforeSplit.tabGroups![0]!.id const secondHostTabId = second.tabId! await runtime.moveMobileSessionTab(`id:${TEST_WORKTREE_ID}`, { kind: 'split', tabId: secondHostTabId, targetGroupId: sourceGroupId, splitDirection: 'right' }) const afterSplit = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(afterSplit.tabGroups).toHaveLength(2) expect(afterSplit.tabGroupLayout).toMatchObject({ type: 'split', direction: 'horizontal' }) // The actual bug: creating a new tab must NOT collapse the split. await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const afterNewTab = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(afterNewTab.tabGroups).toHaveLength(2) expect(afterNewTab.tabGroupLayout).toMatchObject({ type: 'split' }) // The split-off group keeps exactly its one tab; the new tab joins the other. const splitOffGroup = afterNewTab.tabGroups!.find((group) => group.id !== sourceGroupId)! expect(splitOffGroup.tabOrder).toEqual([secondHostTabId]) expect(first.tabId).toBeTruthy() // Regression (#2): reordering one group must not delete the other group. await runtime.moveMobileSessionTab(`id:${TEST_WORKTREE_ID}`, { kind: 'reorder', tabId: secondHostTabId, targetGroupId: splitOffGroup.id, tabOrder: [secondHostTabId] }) const afterReorder = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(afterReorder.tabGroups).toHaveLength(2) }) it('restores a persisted multi-group split on a cold headless rehydrate', async () => { // Regression: hydrate must read back session.tabGroups/tabGroupLayouts, or a server restart coalesces the split into one group. const session = makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'persisted-pty', worktreeId: TEST_WORKTREE_ID, title: 'Left', customTitle: null, color: null, sortOrder: 0, createdAt: 1 }, { id: 'host-tab-2', ptyId: 'persisted-pty-2', worktreeId: TEST_WORKTREE_ID, title: 'Right', customTitle: null, color: null, sortOrder: 1, createdAt: 2 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'persisted-pty' }), 'host-tab-2': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'persisted-pty-2' }) }, tabGroups: { [TEST_WORKTREE_ID]: [ { id: 'group-left', worktreeId: TEST_WORKTREE_ID, activeTabId: 'host-tab', tabOrder: ['host-tab'] }, { id: 'group-right', worktreeId: TEST_WORKTREE_ID, activeTabId: 'host-tab-2', tabOrder: ['host-tab-2'] } ] }, tabGroupLayouts: { [TEST_WORKTREE_ID]: { type: 'split', direction: 'horizontal', first: { type: 'leaf', groupId: 'group-left' }, second: { type: 'leaf', groupId: 'group-right' } } } }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID) const rehydrated = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(rehydrated.tabGroups).toHaveLength(2) expect(rehydrated.tabGroupLayout).toMatchObject({ type: 'split', direction: 'horizontal' }) // Each persisted group keeps its own tab — no coalescing. const left = rehydrated.tabGroups!.find((g) => g.id === 'group-left')! const right = rehydrated.tabGroups!.find((g) => g.id === 'group-right')! expect(left.tabOrder).toEqual(['host-tab']) expect(right.tabOrder).toEqual(['host-tab-2']) }) it('persists a headless terminal rename so it survives a cold rehydrate', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: 'rename-pty' })), write: () => true, kill: () => true, getForegroundProcess: async () => null }) // Bind a live pty to the persisted 'host-tab' so rename resolves by handle. const created = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, activate: true }) await expect(runtime.renameTerminal(created.handle, 'My Title')).resolves.toMatchObject({ title: 'My Title' }) // customTitle must be persisted to the workspace session (not just live pty). const persistedTab = getSession().tabsByWorktree[TEST_WORKTREE_ID]!.find( (tab) => tab.id === 'host-tab' )! expect(persistedTab.customTitle).toBe('My Title') // A cold rehydrate keeps the renamed title. runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID) runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID) const rehydrated = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const renamed = rehydrated.tabs.find( (tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab' ) expect(renamed?.title).toBe('My Title') }) it('persists a headless pane layout (ratio/expand) so it survives a cold rehydrate', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.updateMobileSessionPaneLayout(`id:${TEST_WORKTREE_ID}`, { tabId: 'host-tab', root: { type: 'split', direction: 'vertical', first: { type: 'leaf', leafId: HEADLESS_LEAF_ID }, second: { type: 'leaf', leafId: 'leaf-2' }, ratio: 0.7 }, expandedLeafId: null, titlesByLeafId: { [HEADLESS_LEAF_ID]: 'Pane A' } }) const persisted = getSession().terminalLayoutsByTabId['host-tab']! expect(persisted.root).toMatchObject({ type: 'split', direction: 'vertical', ratio: 0.7 }) expect(persisted.titlesByLeafId).toMatchObject({ [HEADLESS_LEAF_ID]: 'Pane A' }) // Host-owned pty bindings must be preserved through the structural update. expect(persisted.ptyIdsByLeafId).toMatchObject({ [HEADLESS_LEAF_ID]: 'persisted-pty' }) runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID) runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID) const rehydrated = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const surface = rehydrated.tabs.find( (tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab' ) expect(surface?.type === 'terminal' && surface.parentLayout?.root).toMatchObject({ type: 'split', ratio: 0.7 }) }) it('persists headless tab color + pin and surfaces them through a cold rehydrate', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.setMobileSessionTabProps(`id:${TEST_WORKTREE_ID}`, { tabId: 'host-tab', color: '#ff8800', isPinned: true }) const persisted = getSession().tabsByWorktree[TEST_WORKTREE_ID]!.find( (tab) => tab.id === 'host-tab' )! expect(persisted.color).toBe('#ff8800') expect(persisted.isPinned).toBe(true) runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID) runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID) const rehydrated = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const surface = rehydrated.tabs.find( (tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab' ) expect(surface?.type === 'terminal' && surface.color).toBe('#ff8800') expect(surface?.type === 'terminal' && surface.isPinned).toBe(true) }) it('persists headless browser tab color + pin and surfaces them through a cold rehydrate', async () => { const browserTab: Tab = { id: 'browser-page-1', entityId: 'browser-page-1', groupId: 'group-1', worktreeId: TEST_WORKTREE_ID, contentType: 'browser', label: 'Live Browser', customLabel: null, color: null, sortOrder: 1, createdAt: 2, isPreview: false, isPinned: false } const session = makeWorkspaceSessionWithHeadlessTerminal({ unifiedTabs: { [TEST_WORKTREE_ID]: [browserTab] }, tabGroups: { [TEST_WORKTREE_ID]: [ { id: 'group-1', worktreeId: TEST_WORKTREE_ID, activeTabId: 'browser-page-1', tabOrder: ['browser-page-1'] } ] } }) const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setOffscreenBrowserBackend({ createTab: vi.fn(), closeTab: vi.fn() }) runtime.setAgentBrowserBridge({ tabList: vi.fn(() => ({ tabs: [ { browserPageId: 'browser-page-1', index: 0, url: 'https://example.com/', title: 'Live Browser', active: true } ] })) } as never) await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) await runtime.setMobileSessionTabProps(`id:${TEST_WORKTREE_ID}`, { tabId: 'browser-page-1', color: '#3b82f6', isPinned: true }) const persisted = getSession().unifiedTabs?.[TEST_WORKTREE_ID]?.find( (tab) => tab.id === 'browser-page-1' ) expect(persisted?.color).toBe('#3b82f6') expect(persisted?.isPinned).toBe(true) runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID) runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID) const rehydrated = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const surface = rehydrated.tabs.find( (tab) => tab.type === 'browser' && tab.id === 'browser-page-1' ) expect(surface?.type === 'browser' && surface.color).toBe('#3b82f6') expect(surface?.type === 'browser' && surface.isPinned).toBe(true) await runtime.setMobileSessionTabProps(`id:${TEST_WORKTREE_ID}`, { tabId: 'browser-page-1', color: null, isPinned: false }) const cleared = getSession().unifiedTabs?.[TEST_WORKTREE_ID]?.find( (tab) => tab.id === 'browser-page-1' ) expect(cleared?.color).toBeNull() expect(cleared?.isPinned).toBe(false) runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID) runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID) const rehydratedCleared = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const clearedSurface = rehydratedCleared.tabs.find( (tab) => tab.type === 'browser' && tab.id === 'browser-page-1' ) expect(clearedSurface?.type === 'browser' && clearedSurface.color).toBeNull() expect(clearedSurface?.type === 'browser' && clearedSurface.isPinned).toBe(false) }) it('persists headless tab viewMode and surfaces it through a cold rehydrate', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.setMobileSessionTabProps(`id:${TEST_WORKTREE_ID}`, { tabId: 'host-tab', viewMode: 'chat' }) const persisted = getSession().tabsByWorktree[TEST_WORKTREE_ID]!.find( (tab) => tab.id === 'host-tab' )! expect(persisted.viewMode).toBe('chat') const live = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const liveSurface = live.tabs.find( (tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab' ) expect(liveSurface?.type === 'terminal' && liveSurface.viewMode).toBe('chat') runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID) runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID) const rehydrated = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const surface = rehydrated.tabs.find( (tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab' ) expect(surface?.type === 'terminal' && surface.viewMode).toBe('chat') }) it('still persists tab props in serve mode after syncWindowGraph(0) (gate does not fire)', async () => { // Why: serve's syncWindowGraph(0,...) sets authoritativeWindowId=0, but BrowserWindow.fromId(0) is null, so the renderer-authoritative gate must not fire. const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.setMobileSessionTabProps(`id:${TEST_WORKTREE_ID}`, { tabId: 'host-tab', isPinned: true }) expect( getSession().tabsByWorktree[TEST_WORKTREE_ID]!.find((tab) => tab.id === 'host-tab')!.isPinned ).toBe(true) }) it('moves a headless tab into an existing group without renderer_unavailable', async () => { let ptyCounter = 0 const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: `move-group-pty-${++ptyCounter}` })), write: () => true, kill: () => true, getForegroundProcess: async () => null }) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const second = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const before = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const sourceGroupId = before.tabGroups![0]!.id const secondHostTabId = second.tabId! // Split into 2 groups, then move the tab back into the source group. await runtime.moveMobileSessionTab(`id:${TEST_WORKTREE_ID}`, { kind: 'split', tabId: secondHostTabId, targetGroupId: sourceGroupId, splitDirection: 'right' }) const split = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(split.tabGroups).toHaveLength(2) await expect( runtime.moveMobileSessionTab(`id:${TEST_WORKTREE_ID}`, { kind: 'move-to-group', tabId: secondHostTabId, targetGroupId: sourceGroupId }) ).resolves.toEqual({ moved: true }) const merged = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) // Moving the only tab back collapses the split to a single group. expect(merged.tabGroups).toHaveLength(1) expect(merged.tabGroups![0]!.tabOrder).toContain(secondHostTabId) }) it('creates a new headless terminal in the targeted split group, not the active one', async () => { // Regression: a per-group "+" passes targetGroupId, but the headless create ignored it and funneled every new tab into the active group. let ptyCounter = 0 const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: `target-group-pty-${++ptyCounter}` })), write: () => true, kill: () => true, getForegroundProcess: async () => null }) // Why: createMobileSessionTerminal asserts the graph is ready; serve marks it ready via syncWindowGraph(0,...) (windowId 0 ≠ a real renderer). runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const second = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const before = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const leftGroupId = before.tabGroups![0]!.id // Split the 2nd tab into a new right group; the new group becomes active. await runtime.moveMobileSessionTab(`id:${TEST_WORKTREE_ID}`, { kind: 'split', tabId: second.tabId!, targetGroupId: leftGroupId, splitDirection: 'right' }) const split = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(split.tabGroups).toHaveLength(2) const rightGroupId = split.tabGroups!.find((g) => g.id !== leftGroupId)!.id // Create a terminal targeting the LEFT (now non-active) group. await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { targetGroupId: leftGroupId, activate: true }) const after = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const left = after.tabGroups!.find((g) => g.id === leftGroupId)! const right = after.tabGroups!.find((g) => g.id === rightGroupId)! expect(left.tabOrder).toHaveLength(2) // original + the targeted create expect(right.tabOrder).toHaveLength(1) // unchanged }) it('appendBrowserTabOrder keeps a browser in its group across rebuilds (durability)', () => { const runtime = new OrcaRuntimeService(store) const groups = [ { id: 'left', activeTabId: 'web-terminal-a', tabOrder: ['web-terminal-a'] }, { id: 'right', activeTabId: 'web-terminal-b', tabOrder: ['web-terminal-b'] } ] // First create: a new browser targeted at the RIGHT group lands there. const afterCreate = runtime['appendBrowserTabOrder'](groups, ['browser-1'], { tabId: 'browser-1', groupId: 'right' }) expect(afterCreate.find((g) => g.id === 'right')!.tabOrder).toContain('browser-1') expect(afterCreate.find((g) => g.id === 'left')!.tabOrder).not.toContain('browser-1') // Rebuild: the terminal distributor drops the browser id, so appendBrowserTabOrder must restore it to its prior group, not group[0]. const rebuiltGroups = [ { id: 'left', activeTabId: 'web-terminal-a', tabOrder: ['web-terminal-a'] }, { id: 'right', activeTabId: 'web-terminal-b', tabOrder: ['web-terminal-b'] } ] const priorAssignment = runtime['collectBrowserGroupAssignment'](afterCreate, ['browser-1']) const afterRebuild = runtime['appendBrowserTabOrder']( rebuiltGroups, ['browser-1'], undefined, priorAssignment ) expect(afterRebuild.find((g) => g.id === 'right')!.tabOrder).toContain('browser-1') expect(afterRebuild.find((g) => g.id === 'left')!.tabOrder).not.toContain('browser-1') }) it('keeps preserved headless mobile session publication epochs idempotent', async () => { const runtime = new OrcaRuntimeService(store) runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'headless:stable-epoch', snapshotVersion: 1, activeGroupId: null, activeTabId: 'host-tab::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'host-tab::pane:1', parentTabId: 'host-tab', leafId: 'pane:1', title: 'Terminal', isActive: true } ] } ] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [] }) const firstMerge = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [] }) const secondMerge = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(secondMerge.publicationEpoch).toBe(firstMerge.publicationEpoch) expect(secondMerge.publicationEpoch.match(/:headless-merge:/g) ?? []).toHaveLength(1) }) it('briefly preserves abnormal SSH exits for paired pane recovery', async () => { vi.useFakeTimers() try { vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) const runtime = new OrcaRuntimeService(store) const ptyId = 'ssh:ssh-1@@pty-recover' const tabId = 'host-tab' runtime.registerPty(ptyId, TEST_WORKTREE_ID, 'ssh-1', { tabId, leafId: HEADLESS_LEAF_ID }) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-with-ssh-pane', snapshotVersion: 1, activeGroupId: null, activeTabId: `${tabId}::${HEADLESS_LEAF_ID}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `${tabId}::${HEADLESS_LEAF_ID}`, parentTabId: tabId, leafId: HEADLESS_LEAF_ID, ptyId, title: 'Terminal', isActive: true } ] } ] }) runtime.onPtyExit(ptyId, -1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-with-ssh-pane', snapshotVersion: 2, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([ expect.objectContaining({ parentTabId: tabId, status: 'pending-handle' }) ]) vi.advanceTimersByTime(30_001) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-with-ssh-pane', snapshotVersion: 3, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) } finally { vi.useRealTimers() } }) it('briefly preserves an unregistered SSH pane while a restarted HUB rebuilds PTY state', async () => { vi.useFakeTimers() try { vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) const runtime = new OrcaRuntimeService(store) const ptyId = 'ssh:ssh-1@@pty-restart' const tabId = 'host-tab' runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-restarted-hub', snapshotVersion: 1, activeGroupId: null, activeTabId: `${tabId}::${HEADLESS_LEAF_ID}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `${tabId}::${HEADLESS_LEAF_ID}`, parentTabId: tabId, leafId: HEADLESS_LEAF_ID, ptyId, title: 'Terminal', isActive: true } ] } ] }) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-restarted-hub', snapshotVersion: 2, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([ expect.objectContaining({ parentTabId: tabId, status: 'pending-handle' }) ]) vi.advanceTimersByTime(30_001) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-restarted-hub', snapshotVersion: 3, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) } finally { vi.useRealTimers() } }) it('hydrates a persisted SSH-owned pane before an attached renderer publishes its graph', async () => { const ptyId = 'ssh:ssh-1@@pty-persisted' const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId, worktreeId: TEST_WORKTREE_ID, title: 'Persisted SSH Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-after-restart', snapshotVersion: 1, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([ expect.objectContaining({ parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, ptyId, status: 'pending-handle' }) ]) }) it('hydrates a persisted SSH-owned pane when the restarted renderer has not published sessions', async () => { const ptyId = 'ssh:ssh-1@@pty-persisted' const sshSession = makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId, worktreeId: TEST_WORKTREE_ID, title: 'Persisted SSH Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }) } }) const localSession = getDefaultWorkspaceSession() const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const getWorkspaceSession = vi.fn((hostId?: string | null) => hostId === 'ssh:ssh-1' ? sshSession : localSession ) const runtime = new OrcaRuntimeService({ ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getWorkspaceSession } as never) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([ expect.objectContaining({ parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, ptyId, status: 'pending-handle' }) ]) expect(getWorkspaceSession).toHaveBeenCalledWith('ssh:ssh-1') }) it('publishes a recovered SSH pane when its relay becomes ready after an empty restart replay', async () => { const ptyId = 'ssh:ssh-1@@pty-recovered' const sshSession = makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId, worktreeId: TEST_WORKTREE_ID, title: 'Recovered SSH Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }) } }) const localSession = getDefaultWorkspaceSession() const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const runtime = new OrcaRuntimeService({ ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined), getWorkspaceSession: (hostId?: string | null) => hostId === 'ssh:ssh-1' ? sshSession : localSession } as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: ptyId, cwd: TEST_WORKTREE_PATH, title: 'Recovered SSH Terminal' } ] }) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-empty-restart', snapshotVersion: 1, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) const events: RuntimeMobileSessionTabsResult[] = [] runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) runtime.notifySshRelayReady('ssh-1') await vi.waitFor(() => expect( events.some((snapshot) => snapshot.tabs.some( (tab) => tab.type === 'terminal' && tab.ptyId === ptyId && tab.status === 'ready' ) ) ).toBe(true) ) expect(events.at(-1)?.tabs).toEqual([ expect.objectContaining({ parentTabId: 'host-tab', ptyId, status: 'ready', terminal: expect.any(String) }) ]) }) it('uses only a recent expired SSH lease as a bounded pane-recovery tombstone', async () => { vi.useFakeTimers() try { vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: null, worktreeId: TEST_WORKTREE_ID, title: 'Expired SSH Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: undefined }) } }) ) let leaseState: 'expired' | 'terminated' = 'expired' let leaseUpdatedAt = Date.now() const getSshRemotePtyLeases = vi.fn(() => [ { targetId: 'ssh-1', ptyId: 'pty-expired', worktreeId: TEST_WORKTREE_ID, tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, state: leaseState, createdAt: Date.now() - 1_000, updatedAt: leaseUpdatedAt } ]) const runtime = new OrcaRuntimeService({ ...runtimeStore, getSshRemotePtyLeases } as never) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents: { send: vi.fn() } }) const publishEmpty = (snapshotVersion: number): void => { runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-expired-lease', snapshotVersion, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) } publishEmpty(1) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([ expect.objectContaining({ parentTabId: 'host-tab', status: 'pending-handle' }) ]) vi.advanceTimersByTime(30_001) publishEmpty(2) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) leaseState = 'terminated' leaseUpdatedAt = Date.now() publishEmpty(3) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) } finally { vi.useRealTimers() } }) it('does not preserve a normally exited SSH shell for pane recovery', async () => { const runtime = new OrcaRuntimeService(store) const ptyId = 'ssh:ssh-1@@pty-normal-exit' runtime.registerPty(ptyId, TEST_WORKTREE_ID, 'ssh-1', { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-normal-exit', snapshotVersion: 1, activeGroupId: null, activeTabId: `host-tab::${HEADLESS_LEAF_ID}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `host-tab::${HEADLESS_LEAF_ID}`, parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, ptyId, title: 'Terminal', isActive: true } ] } ] }) runtime.onPtyExit(ptyId, 0) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-normal-exit', snapshotVersion: 2, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) }) it('hydrates persisted serve-owned mobile session terminals while a renderer is attached', async () => { const focusTerminal = vi.fn() const spawn = vi.fn().mockResolvedValue({ id: 'serve-persisted-pty', isReattach: true }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'serve-persisted-pty', worktreeId: TEST_WORKTREE_ID, title: 'Persisted Mobile Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'serve-persisted-pty' }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal, closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents: { send: vi.fn() } }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-empty', snapshotVersion: 1, activeGroupId: null, activeTabId: null, activeTabType: null, tabs: [] } ] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs).toEqual([ expect.objectContaining({ type: 'terminal', id: `host-tab::${HEADLESS_LEAF_ID}`, parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, ptyId: 'serve-persisted-pty', status: 'pending-handle' }) ]) expect(listed.tabGroups?.[0]).toMatchObject({ activeTabId: 'host-tab', tabOrder: ['host-tab'] }) const activated = await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, sessionId: 'serve-persisted-pty', persistHostSessionBinding: true, worktreeId: TEST_WORKTREE_ID }) ) expect(focusTerminal).not.toHaveBeenCalled() expect(activated.tabs[0]).toMatchObject({ type: 'terminal', parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, status: 'ready' }) }) function makePendingAgentTabActivationRuntime(opts: { disabledTuiAgents?: string[] } = {}): { runtime: OrcaRuntimeService spawn: ReturnType } { const spawn = vi.fn().mockResolvedValue({ id: 'serve-materialized-pty' }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', customTitle: null, color: null, sortOrder: 0, createdAt: 1, launchAgent: 'claude' } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'serve-dead-pty' }) } }) ) const runtime = new OrcaRuntimeService({ ...runtimeStore, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: opts.disabledTuiAgents ?? [] }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) return { runtime, spawn } } it('launches the pending agent when mobile activation materializes an agent tab', async () => { const { runtime, spawn } = makePendingAgentTabActivationRuntime() const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs[0]).toMatchObject({ type: 'terminal', launchAgent: 'claude', status: 'pending-handle' }) // Why notifyClients false: mirrors the phone tapping the tab, the path that materializes pending tabs headlessly (#7587). const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, `host-tab::${HEADLESS_LEAF_ID}`, undefined, { notifyClients: false } ) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: expect.stringContaining('claude'), sessionId: 'serve-dead-pty', tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, worktreeId: TEST_WORKTREE_ID }) ) expect(activated.tabs[0]).toMatchObject({ type: 'terminal', launchAgent: 'claude', status: 'ready' }) }) it('materializes a plain shell when the pending tab has no launch agent', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'serve-materialized-pty' }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'serve-dead-pty' }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, `host-tab::${HEADLESS_LEAF_ID}`, undefined, { notifyClients: false } ) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ sessionId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID }) ) expect(spawn.mock.calls[0]![0].command).toBeUndefined() }) it('falls back to a plain shell when the pending tab agent is disabled', async () => { const { runtime, spawn } = makePendingAgentTabActivationRuntime({ disabledTuiAgents: ['claude'] }) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, `host-tab::${HEADLESS_LEAF_ID}`, undefined, { notifyClients: false } ) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ sessionId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID }) ) expect(spawn.mock.calls[0]![0].command).toBeUndefined() // Why: the disabled-agent fallback keeps the tab's agent identity; only the startup command is skipped. expect(activated.tabs[0]).toMatchObject({ type: 'terminal', status: 'ready', launchAgent: 'claude' }) }) it('collapses duplicate mobile terminal entries when renderer and headless leaf ids diverge for the same pty', async () => { const rendererLeafId = HEADLESS_SECOND_LEAF_ID const ptyId = 'serve-persisted-pty' const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId, worktreeId: TEST_WORKTREE_ID, title: 'Persisted Mobile Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents: { send: vi.fn() } }) runtime.attachWindow(1) const rendererSnapshot = { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-graph', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `host-tab::${rendererLeafId}`, activeTabType: 'terminal' as const, tabGroups: [ { id: 'group-1', activeTabId: 'host-tab', tabOrder: ['host-tab'] } ], tabs: [ { type: 'terminal' as const, id: `host-tab::${rendererLeafId}`, parentTabId: 'host-tab', leafId: rendererLeafId, ptyId, title: 'Persisted Mobile Terminal', isActive: true } ] } runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [rendererSnapshot] }) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [rendererSnapshot] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const terminalTabs = listed.tabs.filter((tab) => tab.type === 'terminal') expect(listed.tabs).toHaveLength(1) expect(terminalTabs).toHaveLength(1) expect(terminalTabs[0]).toMatchObject({ type: 'terminal', id: `host-tab::${rendererLeafId}`, parentTabId: 'host-tab', leafId: rendererLeafId, ptyId }) }) it('keeps distinct split mobile terminal ptys under the same parent tab', async () => { const rendererLeftLeafId = '33333333-3333-4333-8333-333333333333' const rendererRightLeafId = '44444444-4444-4444-8444-444444444444' const leftPtyId = 'serve-left' const rightPtyId = 'serve-right' const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: leftPtyId, worktreeId: TEST_WORKTREE_ID, title: 'Persisted Split Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: leftPtyId, [HEADLESS_SECOND_LEAF_ID]: rightPtyId }) } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents: { send: vi.fn() } }) runtime.attachWindow(1) const rendererSnapshot = { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-split-graph', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `host-tab::${rendererLeftLeafId}`, activeTabType: 'terminal' as const, tabGroups: [ { id: 'group-1', activeTabId: 'host-tab', tabOrder: ['host-tab'] } ], tabs: [ { type: 'terminal' as const, id: `host-tab::${rendererLeftLeafId}`, parentTabId: 'host-tab', leafId: rendererLeftLeafId, ptyId: leftPtyId, title: 'Left', isActive: true }, { type: 'terminal' as const, id: `host-tab::${rendererRightLeafId}`, parentTabId: 'host-tab', leafId: rendererRightLeafId, ptyId: rightPtyId, title: 'Right', isActive: false } ] } runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [rendererSnapshot] }) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [rendererSnapshot] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const terminalTabs = listed.tabs.filter((tab) => tab.type === 'terminal') expect(listed.tabs).toHaveLength(2) expect(terminalTabs).toHaveLength(2) expect(terminalTabs.map((tab) => tab.ptyId).sort()).toEqual([leftPtyId, rightPtyId]) expect(terminalTabs.map((tab) => tab.leafId).sort()).toEqual( [rendererLeftLeafId, rendererRightLeafId].sort() ) }) it('hydrates legacy persisted terminal tabs without layout entries', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ terminalLayoutsByTabId: {} }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const terminal = listed.tabs[0] expect(terminal).toMatchObject({ type: 'terminal', parentTabId: 'host-tab', ptyId: 'persisted-pty', status: 'pending-handle' }) expect(terminal?.id).toMatch(/^host-tab::[0-9a-f-]{36}$/) }) it('does not mark persisted PTY id collisions ready without matching pane identity', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'persisted-pty', cwd: TEST_WORKTREE_PATH, title: 'Unrelated PTY' } ] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(listed.tabs[0]).toMatchObject({ type: 'terminal', parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, status: 'pending-handle', terminal: null }) }) it('kills persisted SSH PTYs when closing hydrated headless tabs before pane metadata is restored', async () => { const persistedPtyId = 'ssh:ssh-1@@relay-pty' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: persistedPtyId, worktreeId: TEST_WORKTREE_ID, title: 'Remote Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: persistedPtyId }) } }) ) const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [{ id: persistedPtyId, cwd: TEST_WORKTREE_PATH, title: 'Remote' }] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(kill).toHaveBeenCalledWith(persistedPtyId) expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() }) it('durably tears down a runtime-owned SSH headless tab when renderer cleanup fails', async () => { // #8958: the renderer relay can't see headless tabs, so its advisory fallback must not block authoritative teardown/flush. const persistedPtyId = 'ssh:ssh-1@@relay-pty' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: persistedPtyId, worktreeId: TEST_WORKTREE_ID, title: 'Remote Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: persistedPtyId }) } }) ) const kill = vi.fn(() => true) const flushOrThrow = vi.fn() const rendererError = new Error('renderer unavailable') const closeTerminal = vi.fn(() => { throw rendererError }) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const closeTerminalTab = vi.fn(async () => {}) const runtime = new OrcaRuntimeService({ ...runtimeStore, flushOrThrow } as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [{ id: persistedPtyId, cwd: TEST_WORKTREE_PATH, title: 'Remote' }] }) runtime.setNotifier({ closeTerminal, closeTerminalTab } as never) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(closeTerminalTab).not.toHaveBeenCalled() expect(kill).toHaveBeenCalledWith(persistedPtyId) expect(closeTerminal).toHaveBeenCalledWith('host-tab') expect(flushOrThrow).toHaveBeenCalledTimes(1) expect(warn).toHaveBeenCalledWith( '[runtime] failed to notify renderer after headless terminal close', { parentTabId: 'host-tab', error: rendererError } ) expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() }) it('retires an SSH-owned surface when a stale renderer acknowledges close after relay recovery', async () => { const ptyId = 'ssh:ssh-1@@relay-recovered-pty' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId, worktreeId: TEST_WORKTREE_ID, title: 'Recovered SSH Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }) } }) ) const closeTerminal = vi.fn() const closeTerminalTab = vi.fn(async () => {}) let runtime!: OrcaRuntimeService const kill = vi.fn((closedPtyId: string) => { runtime.onPtyExit(closedPtyId, 0) return true }) runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setNotifier({ closeTerminal, closeTerminalTab } as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.registerPty(ptyId, TEST_WORKTREE_ID, 'ssh-1', { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Recovered SSH Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId }, { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_SECOND_LEAF_ID, paneRuntimeId: 2, ptyId: 'stale-renderer-pty' } ] }) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const terminal = listed.tabs.find((tab) => tab.type === 'terminal') if (!terminal || terminal.type !== 'terminal' || !terminal.terminal) { throw new Error('Expected a ready SSH terminal') } await expect(runtime.closeTerminal(terminal.terminal)).resolves.toEqual({ handle: terminal.terminal, tabId: 'host-tab', ptyKilled: true }) expect(closeTerminalTab).toHaveBeenCalledWith('host-tab') expect(closeTerminal).toHaveBeenCalledWith('host-tab') expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) }) it('keeps the renderer close transaction for an adopted runtime-owned tab', async () => { // The renderer pin state can be newer than the debounced session, so once adopted its live close guard must win over stale persisted metadata. const servePtyId = 'serve-adopted-1' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: servePtyId, worktreeId: TEST_WORKTREE_ID, title: 'Adopted Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1, isPinned: false } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: servePtyId }) } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const closeTerminalTab = vi.fn(async () => { throw new Error('terminal_tab_pinned') }) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [{ id: servePtyId, cwd: TEST_WORKTREE_PATH, title: 'Adopted' }] }) runtime.setNotifier({ closeTerminal, closeTerminalTab } as never) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Adopted Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: servePtyId } ] }) await expect( runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') ).rejects.toThrow('terminal_tab_pinned') expect(closeTerminalTab).toHaveBeenCalledWith('host-tab') expect(closeTerminal).not.toHaveBeenCalled() expect(kill).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() }) it('materializes hydrated pending headless terminals with the persisted session identity', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const spawn = vi.fn().mockResolvedValue({ id: 'persisted-pty' }) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const activated = await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, sessionId: 'persisted-pty', persistHostSessionBinding: true, worktreeId: TEST_WORKTREE_ID }) ) expect(activated.tabs[0]).toMatchObject({ type: 'terminal', parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, status: 'ready', terminal: expect.stringMatching(/^term_/) }) }) it('reattaches hydrated SSH headless terminals with the persisted relay identity', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'ssh:ssh-1@@relay-pty', worktreeId: TEST_WORKTREE_ID, title: 'Remote Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'ssh:ssh-1@@relay-pty' }) } }) ) const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const remoteStore = { ...runtimeStore, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined) } const spawn = vi.fn().mockResolvedValue({ id: 'ssh:ssh-1@@relay-pty', isReattach: true }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ connectionId: 'ssh-1', tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, sessionId: 'ssh:ssh-1@@relay-pty', persistHostSessionBinding: true }) ) }) it('spawns fresh after an expired hydrated SSH headless reattach clears persistence', async () => { const stalePtyId = 'ssh:ssh-1@@relay-pty' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: stalePtyId, worktreeId: TEST_WORKTREE_ID, title: 'Remote Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: stalePtyId }) } }) ) const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const remoteStore = { ...runtimeStore, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined) } const spawn = vi .fn() .mockImplementationOnce(async () => { const session = getSession() ;(runtimeStore.setWorkspaceSession as unknown as (next: WorkspaceSessionState) => void)({ ...session, tabsByWorktree: { ...session.tabsByWorktree, [TEST_WORKTREE_ID]: session.tabsByWorktree[TEST_WORKTREE_ID].map((tab) => tab.id === 'host-tab' ? { ...tab, ptyId: null } : tab ) }, terminalLayoutsByTabId: { ...session.terminalLayoutsByTabId, 'host-tab': { ...session.terminalLayoutsByTabId['host-tab'], ptyIdsByLeafId: {} } } }) throw new Error('SSH session expired') }) .mockResolvedValueOnce({ id: 'ssh:ssh-1@@fresh-pty' }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await expect( runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') ).rejects.toThrow('SSH session expired') await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(spawn).toHaveBeenNthCalledWith( 1, expect.objectContaining({ connectionId: 'ssh-1', sessionId: stalePtyId }) ) expect(spawn.mock.calls[1]?.[0]).toEqual( expect.objectContaining({ connectionId: 'ssh-1', tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, persistHostSessionBinding: true }) ) expect(spawn.mock.calls[1]?.[0]).not.toHaveProperty('sessionId') }) it('keeps the activated headless tab active across PTY republishes (serve focus-jump regression)', async () => { // Why: in `orca serve`, focusTerminal has no renderer to persist the remote client's tab choice before PTY republishes. let nextPty = 0 const spawn = vi.fn().mockImplementation(async () => ({ id: `headless-pty-${++nextPty}` })) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const FIRST_LEAF = '22222222-2222-4222-8222-222222222222' const SECOND_LEAF = '33333333-3333-4333-8333-333333333333' // The first-created headless terminal is the one the snapshot marks active. await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'tab-first', leafId: FIRST_LEAF }) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'tab-other', leafId: SECOND_LEAF }) const events: RuntimeMobileSessionTabsResult[] = [] runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) // The remote client switches to the other (non-active) tab. await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'tab-other') const afterActivate = events.at(-1) expect(afterActivate?.activeTabId).toBe(`tab-other::${SECOND_LEAF}`) expect(afterActivate?.activeTabType).toBe('terminal') expect(afterActivate?.tabGroups?.[0]?.activeTabId).toBe('tab-other') expect( afterActivate?.tabs.find((tab) => tab.id === `tab-other::${SECOND_LEAF}`)?.isActive ).toBe(true) expect(afterActivate?.tabs.find((tab) => tab.id === `tab-first::${FIRST_LEAF}`)?.isActive).toBe( false ) // PTY title updates republish snapshots, so the client's chosen tab must survive activation. events.length = 0 runtime.onPtyData('headless-pty-2', '\x1b]0;tab-other running\x07', 200) await waitForMobileSessionTabsEvents(events, 1) const afterPtyData = events.at(-1) expect(afterPtyData?.activeTabId).toBe(`tab-other::${SECOND_LEAF}`) expect(afterPtyData?.activeTabType).toBe('terminal') expect(afterPtyData?.tabGroups?.[0]?.activeTabId).toBe('tab-other') }) it('does not bump the snapshot version when re-activating the already-active headless tab', async () => { // Why: redundant activations of the current tab must not force a remote re-render. const spawn = vi.fn().mockResolvedValue({ id: 'headless-pty-solo' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const LEAF = '44444444-4444-4444-8444-444444444444' await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'tab-solo', leafId: LEAF }) const events: RuntimeMobileSessionTabsResult[] = [] runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'tab-solo') expect(events).toHaveLength(0) }) it('does not persist active server-side when an authoritative renderer is attached', async () => { // Why: an authoritative renderer re-syncs the snapshot itself, so the headless persist must not fire — the renderer stays source of truth. let nextPty = 0 const spawn = vi.fn().mockImplementation(async () => ({ id: `attached-pty-${++nextPty}` })) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const LEAF_A = '55555555-5555-4555-8555-555555555555' const LEAF_B = '66666666-6666-4666-8666-666666666666' await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'tab-a', leafId: LEAF_A }) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'tab-b', leafId: LEAF_B }) // Make an authoritative renderer window present. runtime.attachWindow(1) runtime.markGraphReady(1) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents: { send: vi.fn() } }) const events: RuntimeMobileSessionTabsResult[] = [] runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'tab-b') // The headless persist is gated off by the authoritative window — nothing emitted. expect(events).toHaveLength(0) }) it('does not persist active server-side for a `:headless-merge:` snapshot after renderer detach', async () => { // Why: after renderer detach, merged snapshots have no authoritative window but still carry renderer-owned group state. let nextPty = 0 const spawn = vi.fn().mockImplementation(async () => ({ id: `merge-pty-${++nextPty}` })) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const LEAF_A = '77777777-7777-4777-8777-777777777777' const LEAF_B = '88888888-8888-4888-8888-888888888888' // tab-a (first-created) is the snapshot's active tab. await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'tab-a', leafId: LEAF_A }) await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'tab-b', leafId: LEAF_B }) // Simulate a post-detach merged snapshot with no authoritative window. const current = runtime['mobileSessionTabsByWorktree'].get(TEST_WORKTREE_ID)! runtime['mobileSessionTabsByWorktree'].set(TEST_WORKTREE_ID, { ...current, publicationEpoch: `renderer:headless-merge:${current.publicationEpoch}` }) const events: RuntimeMobileSessionTabsResult[] = [] runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) // The merge exclusion must suppress server-side active rewrites. await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'tab-b') expect(events).toHaveLength(0) }) it('spawns fresh SSH terminals when hydrated persistence has no relay identity', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: null, worktreeId: TEST_WORKTREE_ID, title: 'Remote Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: undefined }) } }) ) const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const remoteStore = { ...runtimeStore, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined) } const spawn = vi.fn().mockResolvedValue({ id: 'ssh:ssh-1@@fresh-pty' }) const runtime = new OrcaRuntimeService(remoteStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ connectionId: 'ssh-1', tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, persistHostSessionBinding: true }) ) expect(spawn.mock.calls[0]?.[0]).not.toHaveProperty('sessionId') }) it('materializes the requested hydrated split leaf instead of the first sibling', async () => { const layout = makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'pty-a', [HEADLESS_SECOND_LEAF_ID]: 'pty-b' }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'pty-a', worktreeId: TEST_WORKTREE_ID, title: 'Persisted Split Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': layout } }) ) const spawn = vi.fn().mockResolvedValue({ id: 'pty-b' }) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const activated = await runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, 'host-tab', HEADLESS_SECOND_LEAF_ID ) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ tabId: 'host-tab', leafId: HEADLESS_SECOND_LEAF_ID, sessionId: 'pty-b' }) ) expect(activated.tabs).toContainEqual( expect.objectContaining({ type: 'terminal', parentTabId: 'host-tab', leafId: HEADLESS_SECOND_LEAF_ID, status: 'ready' }) ) }) it('rejects missing requested split leaves instead of activating a sibling', async () => { const layout = makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'pty-a' }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ terminalLayoutsByTabId: { 'host-tab': layout } }) ) const spawn = vi.fn().mockResolvedValue({ id: 'unexpected-pty' }) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await expect( runtime.activateMobileSessionTab( `id:${TEST_WORKTREE_ID}`, 'host-tab', HEADLESS_SECOND_LEAF_ID ) ).rejects.toThrow('tab_not_found') expect(spawn).not.toHaveBeenCalled() }) it('closes persisted headless terminal parents and kills every live leaf', async () => { const layout = makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'pty-a', [HEADLESS_SECOND_LEAF_ID]: 'pty-b' }) const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'pty-a', worktreeId: TEST_WORKTREE_ID, title: 'Persisted Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': layout } }) ) const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Persisted Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: 'pty-a', paneTitle: 'A' }, { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_SECOND_LEAF_ID, paneRuntimeId: 2, ptyId: 'pty-b', paneTitle: 'B' } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(kill).toHaveBeenCalledWith('pty-a') expect(kill).toHaveBeenCalledWith('pty-b') expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() }) it('closes persisted headless terminal parents before any prior list call', async () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() }) it('tears down a serve-owned headless tab on close while a renderer is attached so it cannot resurrect', async () => { const servePtyId = 'serve-headless-1' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: servePtyId, worktreeId: TEST_WORKTREE_ID, title: 'Serve Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: servePtyId }) } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) // Why: an attached renderer (closeTerminal exists) sends the close down the renderer-attached path that historically leaked serve-owned tabs. runtime.setNotifier({ closeTerminal } as never) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Serve Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: servePtyId, paneTitle: 'A' } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(kill).toHaveBeenCalledWith(servePtyId) // De-persist so syncMobileSessionTabs cannot re-hydrate and resurrect it. expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() // Best-effort renderer notify so no adopted pane is left dead. expect(closeTerminal).toHaveBeenCalledWith('host-tab') }) it('delegates a renderer-owned daemon-session (worktreeId@@uuid) local terminal to the renderer', async () => { // Why: the daemon mints @@ for ordinary renderer-owned terminals too, so id shape alone must not mark it runtime-owned (regression: killed normal locals). const daemonPtyId = `${TEST_WORKTREE_ID}@@d9213842` const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: daemonPtyId, worktreeId: TEST_WORKTREE_ID, title: 'Daemon Session Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: daemonPtyId }) } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ closeTerminal } as never) // Renderer graph PUBLISHES this tab -> it is renderer-owned. runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Daemon Session Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: daemonPtyId, paneTitle: 'A' } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(closeTerminal).toHaveBeenCalledWith('host-tab') expect(kill).not.toHaveBeenCalled() // Not torn down by the runtime — left for the renderer's own close to prune. expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() }) it('tears down a leaked daemon-session headless tab the renderer never published', async () => { // Why: same @@ id but absent from the renderer graph — a real leak that must be de-persisted so syncMobileSessionTabs can't resurrect it. const daemonPtyId = `${TEST_WORKTREE_ID}@@77e25ca0` const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: daemonPtyId, worktreeId: TEST_WORKTREE_ID, title: 'Leaked Daemon Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: daemonPtyId }) } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ closeTerminal } as never) // Empty renderer graph -> the host's tab was never published by the renderer. runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() expect(closeTerminal).toHaveBeenCalledWith('host-tab') }) it('defers a renderer-published pending tab to the renderer instead of tearing it down', async () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ closeTerminal } as never) // Pending tab in the renderer graph (PTY not bound yet) is renderer-owned, so the runtime forwards the close but must not de-persist it. runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Pending Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'headless:pending', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `host-tab::${HEADLESS_LEAF_ID}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `host-tab::${HEADLESS_LEAF_ID}`, parentTabId: 'host-tab', leafId: HEADLESS_LEAF_ID, title: 'Pending Terminal', isActive: true } ] } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(closeTerminal).toHaveBeenCalledWith('host-tab') expect(kill).not.toHaveBeenCalled() // Not torn down by the runtime: the renderer-owned tab is left for the renderer's own close to prune. expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() }) it('tears down a runtime pending shell the renderer never adopted on close', async () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ closeTerminal } as never) // Empty graph → this persisted headless shell (no live PTY) is runtime-owned and must be torn down or it re-hydrates on next publish. runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab') expect(kill).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() expect(closeTerminal).toHaveBeenCalledWith('host-tab') }) it('closes only the addressed serve-owned split leaf so siblings survive even with a renderer attached', async () => { const layout = makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'serve-left', [HEADLESS_SECOND_LEAF_ID]: 'serve-right' }) const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'serve-left', worktreeId: TEST_WORKTREE_ID, title: 'Split Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': layout } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ closeTerminal } as never) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Split Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: 'serve-left', paneTitle: 'L' }, { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_SECOND_LEAF_ID, paneRuntimeId: 2, ptyId: 'serve-right', paneTitle: 'R' } ] }) await runtime.closeMobileSessionTab( `id:${TEST_WORKTREE_ID}`, `host-tab::${HEADLESS_SECOND_LEAF_ID}` ) // Exact split leaf: kill only that leaf's PTY, keep the sibling, don't tear down the parent. expect(kill).toHaveBeenCalledWith('serve-right') expect(kill).not.toHaveBeenCalledWith('serve-left') expect(closeTerminal).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() }) describe('close intent adjudication', () => { // Shared setup: a renderer-adopted tab whose PTY the host sees alive. function makeAdoptedLiveTabRuntime(): { runtime: OrcaRuntimeService getSession: () => WorkspaceSessionState kill: ReturnType closeTerminal: ReturnType closeTerminalTab: ReturnType listProcesses: ReturnType processes: { id: string; cwd: string; title: string }[] } { const servePtyId = 'serve-live-1' const processes = [{ id: servePtyId, cwd: TEST_WORKTREE_PATH, title: 'Live' }] const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: servePtyId, worktreeId: TEST_WORKTREE_ID, title: 'Live Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: servePtyId }) } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const closeTerminalTab = vi.fn(async () => {}) const listProcesses = vi.fn(async () => processes) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses }) runtime.setNotifier({ closeTerminal, closeTerminalTab } as never) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Live Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: servePtyId } ] }) return { runtime, getSession, kill, closeTerminal, closeTerminalTab, listProcesses, processes } } it.each(['pty-exit', 'cleanup'] as const)( 'refuses a %s echoed close while the PTY is live and republishes the snapshot', async (reason) => { const { runtime, getSession, kill, closeTerminal, closeTerminalTab } = makeAdoptedLiveTabRuntime() const before = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const events: { worktree: string; snapshotVersion: number; tabs: unknown[] }[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) const result = await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason }) unsubscribe() // No destructive branch may run: no PTY kill, no renderer close relay. expect(result).toEqual({ closed: true, refused: true, refusalReason: 'live-host-pty', snapshotRepublished: true }) expect(kill).not.toHaveBeenCalled() expect(closeTerminalTab).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) // The snapshot is republished (version bumped) so the echoing client // re-adds and re-attaches the still-live tab. const republished = events.filter((event) => event.worktree === TEST_WORKTREE_ID) expect(republished.length).toBeGreaterThan(0) const last = republished.at(-1)! expect(last.snapshotVersion).toBeGreaterThan(before.snapshotVersion) expect( last.tabs.some((tab) => (tab as { parentTabId?: string }).parentTabId === 'host-tab') ).toBe(true) } ) it('coalesces a reconnect close burst onto one authoritative PTY inventory', async () => { const { runtime, listProcesses, processes } = makeAdoptedLiveTabRuntime() const inventory = deferred() listProcesses.mockImplementation(() => inventory.promise) const closes = Promise.all([ runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit' }), runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'cleanup' }) ]) await vi.waitFor(() => expect(listProcesses).toHaveBeenCalledTimes(1)) inventory.resolve(processes) await expect(closes).resolves.toEqual([ expect.objectContaining({ refused: true, refusalReason: 'live-host-pty' }), expect.objectContaining({ refused: true, refusalReason: 'live-host-pty' }) ]) expect(listProcesses).toHaveBeenCalledTimes(1) }) it('keeps a live persisted PTY whose pane binding has not reconnected yet', async () => { const ptyId = 'persisted-pty' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId, worktreeId: TEST_WORKTREE_ID, title: 'Persisted Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }) } }) ) const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [{ id: ptyId, cwd: TEST_WORKTREE_PATH, title: 'Live' }] }) const result = await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'cleanup' }) expect(result).toEqual({ closed: true, refused: true, refusalReason: 'live-host-pty', snapshotRepublished: true }) expect(kill).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() }) it('keeps an explicit user close destructive while the PTY is live', async () => { const { runtime, closeTerminalTab } = makeAdoptedLiveTabRuntime() await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'user' }) // Legacy whole-parent relay: the renderer close transaction still runs. expect(closeTerminalTab).toHaveBeenCalledWith('host-tab') }) it('keeps a reasonless legacy close and republishes its live mirror', async () => { const { runtime, kill, closeTerminal, closeTerminalTab } = makeAdoptedLiveTabRuntime() const before = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const result = await runtime.refuseUnattributedMobileSessionTabClose( `id:${TEST_WORKTREE_ID}`, 'host-tab' ) const after = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(result).toEqual({ closed: true, refused: true, refusalReason: 'missing-intent', snapshotRepublished: true }) expect(after.snapshotVersion).toBeGreaterThan(before.snapshotVersion) expect(kill).not.toHaveBeenCalled() expect(closeTerminalTab).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() }) it('refuses a lifecycle close from a stale host publication', async () => { const { runtime, kill, closeTerminal, closeTerminalTab } = makeAdoptedLiveTabRuntime() const current = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const terminal = current.tabs.find((tab) => tab.type === 'terminal') if (!terminal || terminal.status !== 'ready') { throw new Error('expected a ready terminal fixture') } const result = await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit', expectedPublicationEpoch: 'stale-epoch', expectedTerminalHandle: terminal.terminal }) expect(result).toEqual({ closed: true, refused: true, refusalReason: 'stale-publication', snapshotRepublished: true }) expect(kill).not.toHaveBeenCalled() expect(closeTerminalTab).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() }) it('refuses a reused tab id that names a different terminal incarnation', async () => { const { runtime, kill, closeTerminal, closeTerminalTab } = makeAdoptedLiveTabRuntime() const current = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const result = await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit', expectedPublicationEpoch: current.publicationEpoch, expectedTerminalHandle: 'term-from-retired-incarnation' }) expect(result).toEqual({ closed: true, refused: true, refusalReason: 'stale-terminal', snapshotRepublished: true }) expect(kill).not.toHaveBeenCalled() expect(closeTerminalTab).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() }) it('leaves dead renderer-owned retirement to the renderer without relaying a close', async () => { const { runtime, processes, kill, closeTerminal, closeTerminalTab } = makeAdoptedLiveTabRuntime() const current = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const terminal = current.tabs.find((tab) => tab.type === 'terminal') if (!terminal || terminal.status !== 'ready') { throw new Error('expected a ready terminal fixture') } runtime.onPtyExit('serve-live-1', 0) processes.length = 0 const result = await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit', expectedPublicationEpoch: current.publicationEpoch, expectedTerminalHandle: terminal.terminal }) expect(result).toEqual({ closed: true, refused: true, refusalReason: 'retirement-owner' }) expect(kill).not.toHaveBeenCalled() expect(closeTerminalTab).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() }) function makeSplitLeafRuntime(): { runtime: OrcaRuntimeService getSession: () => WorkspaceSessionState kill: ReturnType closeTerminal: ReturnType } { const layout = makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'serve-left', [HEADLESS_SECOND_LEAF_ID]: 'serve-right' }) const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: 'serve-left', worktreeId: TEST_WORKTREE_ID, title: 'Split Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': layout } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ closeTerminal } as never) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Split Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: 'serve-left', paneTitle: 'L' }, { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_SECOND_LEAF_ID, paneRuntimeId: 2, ptyId: 'serve-right', paneTitle: 'R' } ] }) return { runtime, getSession, kill, closeTerminal } } it('refuses a pty-exit echoed close of a live split leaf (direct-kill branch)', async () => { const { runtime, getSession, kill, closeTerminal } = makeSplitLeafRuntime() await runtime.closeMobileSessionTab( `id:${TEST_WORKTREE_ID}`, `host-tab::${HEADLESS_SECOND_LEAF_ID}`, { reason: 'pty-exit' } ) expect(kill).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() }) it('still kills a live split leaf for an explicit user close', async () => { const { runtime, kill } = makeSplitLeafRuntime() await runtime.closeMobileSessionTab( `id:${TEST_WORKTREE_ID}`, `host-tab::${HEADLESS_SECOND_LEAF_ID}`, { reason: 'user' } ) expect(kill).toHaveBeenCalledWith('serve-right') expect(kill).not.toHaveBeenCalledWith('serve-left') }) it('refuses without republishing when the echoed leaf is dead but a sibling is live', async () => { // Why: the only reachable close path for a single leaf destroys the whole // parent (live sibling included), so the close must be refused — but a // republish would re-add the dead leaf on the echoing client and feed a // refuse→republish→re-echo loop. const { runtime, getSession, kill, closeTerminal } = makeSplitLeafRuntime() runtime.onPtyExit('serve-right', 0) const events: { worktree: string }[] = [] const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot)) const result = await runtime.closeMobileSessionTab( `id:${TEST_WORKTREE_ID}`, `host-tab::${HEADLESS_SECOND_LEAF_ID}`, { reason: 'pty-exit' } ) unsubscribe() expect(result).toEqual({ closed: true, refused: true, refusalReason: 'live-host-pty' }) expect(kill).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(events.filter((event) => event.worktree === TEST_WORKTREE_ID)).toEqual([]) }) it('refuses a pty-exit echoed close of a runtime-owned headless tab with a live PTY', async () => { // Why: the headless close path kills every leaf PTY and de-persists the // parent; an echo must not reach it while the host sees the PTY alive. const servePtyId = 'serve-headless-live' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'host-tab', ptyId: servePtyId, worktreeId: TEST_WORKTREE_ID, title: 'Serve Terminal', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: servePtyId }) } }) ) const kill = vi.fn(() => true) const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.setNotifier({ closeTerminal } as never) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Serve Terminal', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: servePtyId, paneTitle: 'A' } ] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit' }) expect(kill).not.toHaveBeenCalled() expect(closeTerminal).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() }) it('retires a dead headless tab on a pty-exit echoed close', async () => { // Why: headless hosts have no renderer pty-exit handling of their own; // they rely on the client echo to retire genuinely dead tab records. const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit' }) expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() }) it('retires a dead headless tab whose exited PTY still has a retained record', async () => { // Why: onPtyExit keeps the disconnected record in ptysById for status and // exit reads — the production state after a real exit. The gate must not // read record presence as liveness or the dead tab never retires and the // client echo loops. const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const processes: { id: string; cwd: string; title: string }[] = [ { id: 'persisted-pty', cwd: TEST_WORKTREE_PATH, title: 'Shell' } ] const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => processes }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) // Seed the connected PTY record from the controller listing, then let the // process die: the record flips to disconnected but stays retained. await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) runtime.onPtyExit('persisted-pty', 0) processes.length = 0 await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit' }) expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined() expect(kill).not.toHaveBeenCalled() }) it('keeps a headless tab when the provider inventory is unavailable', async () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const kill = vi.fn(() => true) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null, listProcesses: async () => { throw new Error('access denied') } }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const result = await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', { reason: 'pty-exit' }) expect(result).toEqual({ closed: true, refused: true, refusalReason: 'unknown-liveness', snapshotRepublished: true }) expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toHaveLength(1) expect(getSession().terminalLayoutsByTabId['host-tab']).toBeDefined() expect(kill).not.toHaveBeenCalled() }) }) it('builds mobile session agent launch commands on the runtime host', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: { 'command-code': 'command-code --profile mobile' }, agentDefaultEnv: { 'command-code': { COMMAND_CODE_PROFILE: 'mobile-env' } } }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'command-code' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: "command-code --profile mobile '--yolo'", cwd: TEST_WORKTREE_PATH, env: expect.objectContaining({ COMMAND_CODE_PROFILE: 'mobile-env' }), worktreeId: TEST_WORKTREE_ID }) ) }) it('injects mobile quick-command prompts into the host-built agent startup command', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent-prompt' }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: { codex: 'codex' }, agentDefaultArgs: {} }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'codex', agentPrompt: 'Review this diff' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: expect.stringMatching(/^codex .*'Review this diff'$/), launchAgent: 'codex', cwd: TEST_WORKTREE_PATH }) ) }) it('rejects startup prompts for agents that require post-ready stdin', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent-prompt' }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: {} }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await expect( runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'aider', agentPrompt: 'Review this diff' }) ).rejects.toThrow('does not support startup prompt quick commands') expect(spawn).not.toHaveBeenCalled() }) it('uses POSIX quoting for mobile agent launch commands in WSL project runtimes', async () => { await withPlatform('win32', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) const runtime = new OrcaRuntimeService({ ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: { 'command-code': 'command-code --profile mobile' }, agentDefaultArgs: { 'command-code': '--note "can\'t"' }, localWindowsRuntimeDefault: { kind: 'windows-host' } }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'command-code' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: "command-code --profile mobile '--note' 'can'\\''t'", cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID }) ) }) }) it('keeps PowerShell quoting for mobile agent launch commands in Windows host runtimes', async () => { await withPlatform('win32', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) const runtime = new OrcaRuntimeService({ ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'windows-host' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: { 'command-code': 'command-code --profile mobile' }, agentDefaultArgs: { 'command-code': '--note "can\'t"' }, localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'command-code' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: "command-code --profile mobile '--note' 'can''t'", cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID }) ) }) }) it('uses cmd.exe quoting for mobile agent launch commands in local Windows host runtimes', async () => { await withPlatform('win32', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent-cmd' }) const runtime = new OrcaRuntimeService({ ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'windows-host' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: { 'command-code': 'command-code --profile mobile' }, agentDefaultArgs: { 'command-code': '--note "can\'t"' }, localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }, terminalWindowsShell: 'cmd.exe' }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'command-code' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ command: 'command-code --profile mobile "--note" "can\'t"', cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID }) ) }) }) it('publishes headless mobile session agent identity with synthesized PTY status', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: {} }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const created = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'claude' }) runtime.onPtyData('pty-agent', '\x1b]0;✳ Claude Code\x07', Date.now()) const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect(created.tab).toMatchObject({ type: 'terminal', launchAgent: 'claude' }) expect(listed.tabs).toEqual([ expect.objectContaining({ type: 'terminal', launchAgent: 'claude', agentStatus: expect.objectContaining({ state: 'done', agentType: 'claude' }) }) ]) }) it('rejects disabled mobile session agent launches before spawning', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: ['codex'], agentCmdOverrides: {} }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await expect( runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { agent: 'codex' }) ).rejects.toThrow('Selected agent is disabled') expect(spawn).not.toHaveBeenCalled() }) it('validates mobile terminal insertion anchors before resolving agent launch commands', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: ['codex'], agentCmdOverrides: {} }) } as never) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await expect( runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { afterTabId: 'stale-tab', agent: 'codex' }) ).rejects.toThrow('after_tab_not_found') expect(spawn).not.toHaveBeenCalled() }) it('forwards inactive mobile terminal creation to the renderer without focusing it', async () => { const focusTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ focusTerminal, worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), closeTerminal: vi.fn(), closeSessionTab: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string; activate?: boolean }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: { send: vi.fn() } }, { requestId: payload.requestId, error: 'spoofed renderer reply' } ) runtime.syncWindowGraph(1, { tabs: [], leaves: [ { tabId: 'tab-renderer', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-renderer', paneTitle: null } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: null, activeTabType: null, tabs: [ { type: 'terminal', id: 'tab-renderer::pane:1', parentTabId: 'tab-renderer', leafId: 'pane:1', title: 'Terminal', isActive: false } ] } ] }) ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-renderer', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false, viewMode: 'chat' }) expect(send).toHaveBeenCalledWith( 'terminal:requestTabCreate', expect.objectContaining({ worktreeId: TEST_WORKTREE_ID, activate: false, source: 'runtime-session', viewMode: 'chat' }) ) expect(focusTerminal).not.toHaveBeenCalled() expect(result.tab).toMatchObject({ parentTabId: 'tab-renderer', isActive: false }) }) it('dedupes concurrent mobile terminal creates that share a clientMutationId', async () => { const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ focusTerminal: vi.fn(), worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), closeTerminal: vi.fn(), closeSessionTab: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { runtime.syncWindowGraph(1, { tabs: [], leaves: [ { tabId: 'tab-renderer', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-renderer', paneTitle: null } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: null, activeTabType: null, tabs: [ { type: 'terminal', id: 'tab-renderer::pane:1', parentTabId: 'tab-renderer', leafId: 'pane:1', title: 'Terminal', isActive: false } ] } ] }) ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-renderer', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const [first, second] = await Promise.all([ runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false, clientMutationId: 'mutation-1' }), runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false, clientMutationId: 'mutation-1' }) ]) const createRequests = send.mock.calls.filter( ([channel]) => channel === 'terminal:requestTabCreate' ) expect(createRequests).toHaveLength(1) expect(second).toBe(first) expect(first.tab).toMatchObject({ parentTabId: 'tab-renderer' }) }) it('returns the settled success for a retried clientMutationId whose response was lost', async () => { const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ focusTerminal: vi.fn(), worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), closeTerminal: vi.fn(), closeSessionTab: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { runtime.syncWindowGraph(1, { tabs: [], leaves: [ { tabId: 'tab-renderer', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-renderer', paneTitle: null } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: null, activeTabType: null, tabs: [ { type: 'terminal', id: 'tab-renderer::pane:1', parentTabId: 'tab-renderer', leafId: 'pane:1', title: 'Terminal', isActive: false } ] } ] }) ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-renderer', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const first = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false, clientMutationId: 'mutation-lost-response' }) // Why: the phone retries the same key when the create response was lost; within the retention window it must reuse the terminal. const retried = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false, clientMutationId: 'mutation-lost-response' }) const createRequests = send.mock.calls.filter( ([channel]) => channel === 'terminal:requestTabCreate' ) expect(createRequests).toHaveLength(1) expect(retried).toBe(first) }) it('does not dedupe mobile terminal creates across worktrees with the same clientMutationId', async () => { const otherWorktreeId = `${TEST_REPO_ID}::/tmp/worktree-b` vi.mocked(listWorktrees).mockResolvedValue([ ...MOCK_GIT_WORKTREES, { path: '/tmp/worktree-b', head: 'def', branch: 'feature/bar', isBare: false, isMainWorktree: false } ]) const runtimeStore = { ...store, getAllWorktreeMeta: () => ({ [TEST_WORKTREE_ID]: store.getAllWorktreeMeta()[TEST_WORKTREE_ID], [otherWorktreeId]: makeWorktreeMeta({ displayName: 'other' }) }) } const runtime = new OrcaRuntimeService(runtimeStore) runtime.setNotifier({ focusTerminal: vi.fn(), worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), closeTerminal: vi.fn(), closeSessionTab: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string; worktreeId: string }) => { const parentTabId = payload.worktreeId === TEST_WORKTREE_ID ? 'tab-renderer-a' : 'tab-renderer-b' ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: parentTabId, title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const firstCreate = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false, clientMutationId: 'mutation-1' }) const secondCreate = runtime.createMobileSessionTerminal(`id:${otherWorktreeId}`, { activate: false, clientMutationId: 'mutation-1' }) await vi.waitFor(() => { const createRequests = send.mock.calls.filter( ([channel]) => channel === 'terminal:requestTabCreate' ) expect(createRequests).toHaveLength(2) }) runtime.syncWindowGraph(1, { tabs: [], leaves: [ { tabId: 'tab-renderer-a', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-renderer-a', paneTitle: null }, { tabId: 'tab-renderer-b', worktreeId: otherWorktreeId, leafId: 'pane:1', paneRuntimeId: 2, ptyId: 'pty-renderer-b', paneTitle: null } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-a', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: null, activeTabType: null, tabs: [ { type: 'terminal', id: 'tab-renderer-a::pane:1', parentTabId: 'tab-renderer-a', leafId: 'pane:1', title: 'Terminal', isActive: false } ] }, { worktree: otherWorktreeId, publicationEpoch: 'epoch-b', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: null, activeTabType: null, tabs: [ { type: 'terminal', id: 'tab-renderer-b::pane:1', parentTabId: 'tab-renderer-b', leafId: 'pane:1', title: 'Terminal', isActive: false } ] } ] }) const [first, second] = await Promise.all([firstCreate, secondCreate]) const createRequests = send.mock.calls.filter( ([channel]) => channel === 'terminal:requestTabCreate' ) expect(createRequests).toHaveLength(2) expect(first.tab).toMatchObject({ parentTabId: 'tab-renderer-a' }) expect(second.tab).toMatchObject({ parentTabId: 'tab-renderer-b' }) }) it('materializes a renderer-created mobile terminal whose surface stays pending', async () => { vi.useFakeTimers() try { const pendingLeafId = '33333333-3333-4333-8333-333333333333' const closeTerminal = vi.fn() const revealTerminalSession = vi.fn() const spawn = vi.fn().mockResolvedValue({ id: 'pty-materialized' }) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ focusTerminal: vi.fn(), worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), closeTerminal, closeSessionTab: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-pending', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true, viewMode: 'terminal' }) let settled = false const settledCreate = create.finally(() => { settled = true }) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-pending', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `tab-pending::${pendingLeafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-pending::${pendingLeafId}`, parentTabId: 'tab-pending', leafId: pendingLeafId, title: 'Terminal', isActive: true } ] } ] }) await vi.advanceTimersByTimeAsync(999) expect(settled).toBe(false) expect(spawn).not.toHaveBeenCalled() await vi.advanceTimersByTimeAsync(1) const result = await settledCreate expect(result.tab).toMatchObject({ type: 'terminal', parentTabId: 'tab-pending', leafId: pendingLeafId, status: 'ready', terminal: expect.stringMatching(/^term_/), viewMode: 'terminal', isActive: true }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID, tabId: 'tab-pending', leafId: pendingLeafId, persistHostSessionBinding: true, preAllocatedHandle: expect.stringMatching(/^term_/) }) ) expect(revealTerminalSession).toHaveBeenCalledWith( TEST_WORKTREE_ID, expect.objectContaining({ ptyId: 'pty-materialized', tabId: 'tab-pending', leafId: pendingLeafId, viewMode: 'terminal' }) ) expect(closeTerminal).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('rolls back a half-created terminal whose surface never publishes', async () => { vi.useFakeTimers() try { const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ focusTerminal: vi.fn(), worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), closeTerminal, closeSessionTab: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) // Why: reply with a tabId but never sync a surface graph, so waitForMobileTerminalSurface times out and rollback runs. const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-ghost', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const pending = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false }) const settled = pending.then( () => ({ ok: true as const }), (error: Error) => ({ ok: false as const, error }) ) await vi.advanceTimersByTimeAsync(11_000) const outcome = await settled expect(outcome.ok).toBe(false) expect(closeTerminal).toHaveBeenCalledWith('tab-ghost') } finally { vi.useRealTimers() } }) // Why: the five #7587 mobile-create tests share one notifier factory so interface changes live in one place. function createMobileCreateTestNotifier( closeTerminal: (tabId: string, paneRuntimeId?: number) => void ) { return { focusTerminal: vi.fn(), worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), closeTerminal, closeSessionTab: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() } } it('rolls back a mobile create when the materialize spawn fails and no live PTY backs the tab', async () => { vi.useFakeTimers() try { const pendingLeafId = '99999999-9999-4999-8999-999999999999' const closeTerminal = vi.fn() // Why: #7587 rescue is gated on a live PTY, not on a mere surface — else this handle-less failed-spawn dead shell would resolve as success. const spawn = vi.fn().mockRejectedValue(new Error('spawn failed')) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier(createMobileCreateTestNotifier(closeTerminal)) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-pending', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) const settled = create.then( () => ({ ok: true as const }), (error: Error) => ({ ok: false as const, error }) ) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) // Only the tab shell publishes (no ptyId → never ready); no PTY ever binds. runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-pending', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `tab-pending::${pendingLeafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-pending::${pendingLeafId}`, parentTabId: 'tab-pending', leafId: pendingLeafId, title: 'Terminal', isActive: true } ] } ] }) // Ready-fallback (1s) expires → materialize runs → spawn rejects → catch. await vi.advanceTimersByTimeAsync(2_000) const outcome = await settled expect(outcome.ok).toBe(false) expect(closeTerminal).toHaveBeenCalledWith('tab-pending') } finally { vi.useRealTimers() } }) it('keeps a mobile-created terminal alive when the renderer never publishes the surface', async () => { vi.useFakeTimers() try { const leafId = '44444444-4444-4444-8444-444444444444' const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier(createMobileCreateTestNotifier(closeTerminal)) // Why: reply with a tabId but never sync a matching graph, reproducing a renderer that spawns the PTY but stalls graph-sync past the surface timeout (#7587). const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-alive', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true, viewMode: 'chat' }) let settled = false const settledCreate = create.finally(() => { settled = true }) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) // A shell-only snapshot can win the first race but omit launch props; the later PTY rescue must fill the explicit mode. runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'renderer-shell', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: `tab-alive::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-alive::${leafId}`, parentTabId: 'tab-alive', leafId, title: 'Terminal', isActive: true } ] } ] }) // The renderer's PTY spawn registers with the tab binding (as the pty IPC layer now does) after the shell-only snapshot. runtime.registerPty('pty-alive', TEST_WORKTREE_ID, null, { tabId: 'tab-alive', leafId }) // Resolves promptly, well under MOBILE_TERMINAL_SURFACE_TIMEOUT_MS (10s). await vi.advanceTimersByTimeAsync(50) const result = await settledCreate expect(settled).toBe(true) expect(result.tab).toMatchObject({ type: 'terminal', parentTabId: 'tab-alive', leafId, status: 'ready', terminal: expect.stringMatching(/^term_/), viewMode: 'chat' }) expect(closeTerminal).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('keeps a mobile-created terminal alive when the renderer PTY spawn races ahead of the reply', async () => { vi.useFakeTimers() try { const leafId = '55555555-5555-4555-8555-555555555555' const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier(createMobileCreateTestNotifier(closeTerminal)) // Why: spawn and tabCreate reply are independent IPC channels; here the PTY registers before the reply, so the pre-wait check resolves it. const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { runtime.registerPty('pty-early', TEST_WORKTREE_ID, null, { tabId: 'tab-early', leafId }) ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-early', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) let settled = false const settledCreate = create.finally(() => { settled = true }) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) // Resolves via the immediate pre-wait rescue, well under the 10s timeout (not the catch path). await vi.advanceTimersByTimeAsync(50) expect(settled).toBe(true) const result = await settledCreate expect(result.tab).toMatchObject({ type: 'terminal', parentTabId: 'tab-early', leafId, status: 'ready', terminal: expect.stringMatching(/^term_/) }) expect(closeTerminal).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('keeps a mobile-created terminal alive when the renderer snapshot is rejected by the version guard', async () => { vi.useFakeTimers() try { const leafId = '66666666-6666-4666-8666-666666666666' const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier(createMobileCreateTestNotifier(closeTerminal)) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-guard', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) // Why: seed an inflated stored version under a stable epoch (the state prior renderer publications leave behind). runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-guard', snapshotVersion: 50, activeGroupId: 'group-guard', activeTabId: null, activeTabType: null, tabs: [] } ] }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) let settled = false const settledCreate = create.finally(() => { settled = true }) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) // Renderer republishes with a stale (lower) version under the same epoch, so syncMobileSessionTabs rejects it — the reporter's stall variant (#7587). runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-guard', snapshotVersion: 1, activeGroupId: 'group-guard', activeTabId: `tab-guard::${leafId}`, activeTabType: 'terminal', tabs: [ { type: 'terminal', id: `tab-guard::${leafId}`, parentTabId: 'tab-guard', leafId, title: 'Terminal', isActive: true } ] } ] }) await vi.advanceTimersByTimeAsync(50) // The rejected renderer sync must not have resolved the create. expect(settled).toBe(false) // Prove the rejection: the stored snapshot still lacks the tab. const beforeRescue = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) expect( beforeRescue.tabs.some((tab) => tab.type === 'terminal' && tab.parentTabId === 'tab-guard') ).toBe(false) // The renderer's own PTY spawn registers with the binding and rescues it. runtime.registerPty('pty-guard', TEST_WORKTREE_ID, null, { tabId: 'tab-guard', leafId }) await vi.advanceTimersByTimeAsync(50) const result = await settledCreate expect(settled).toBe(true) expect(result.tab).toMatchObject({ type: 'terminal', parentTabId: 'tab-guard', leafId, status: 'ready', terminal: expect.stringMatching(/^term_/) }) expect(closeTerminal).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('keeps a mobile-created terminal alive at the surface timeout when only a leaf-synced PTY backs the tab', async () => { vi.useFakeTimers() try { const leafId = '77777777-7777-4777-8777-777777777777' const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier(createMobileCreateTestNotifier(closeTerminal)) // Why (#7587): leaf graph-sync lands identity without registerPty, so only the catch-path rescue saves the stalled live session. const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-catch', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: true }) let settled = false const settledCreate = create.finally(() => { settled = true }) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) // Let the create clear its pre-wait check and park in waitForMobileTerminalSurface before any identity arrives. await vi.advanceTimersByTimeAsync(50) // Identity arrives via leaf graph-sync, NOT registerPty, so no rescue fires and the surface is never published. runtime.syncWindowGraph(1, { tabs: [], leaves: [ { tabId: 'tab-catch', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-catch' } ] }) // Surface still unpublished, so the create is still pending. expect(settled).toBe(false) // Cross the 10s surface timeout: the catch path must rescue from the live PTY, not roll the session back destructively. await vi.advanceTimersByTimeAsync(11_000) const result = await settledCreate expect(settled).toBe(true) expect(result.tab).toMatchObject({ type: 'terminal', parentTabId: 'tab-catch', leafId, status: 'ready', terminal: expect.stringMatching(/^term_/) }) expect(closeTerminal).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('reports browser tab creation as unsupported for a windowless host with no offscreen backend', async () => { const runtime = new OrcaRuntimeService(store) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) await expect( runtime.browserTabCreate({ worktree: `id:${TEST_WORKTREE_ID}`, url: 'https://example.com' }) ).rejects.toMatchObject({ code: 'browser_error', message: expect.stringContaining('does not support browser panes') }) }) it('creates a browser tab via the offscreen backend for a headless runtime server', async () => { const runtime = new OrcaRuntimeService(store) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) const createTab = vi.fn(async () => ({ browserPageId: 'page-headless' })) runtime.setOffscreenBrowserBackend({ createTab, closeTab: vi.fn() }) await expect( runtime.browserTabCreate({ worktree: `id:${TEST_WORKTREE_ID}`, url: 'https://example.com' }) ).resolves.toEqual({ browserPageId: 'page-headless' }) expect(createTab).toHaveBeenCalledWith(expect.objectContaining({ url: 'https://example.com' })) }) it('cancels an in-flight same-connection browser screencast before replacing it', async () => { const runtime = createRuntime() const firstStart = deferred<{ subscriptionId: string ready: never session: { stop: () => void; done: Promise } }>() const firstDone = deferred() const secondDone = deferred() const thirdDone = deferred() const firstStop = vi.fn(() => firstDone.resolve()) const secondStop = vi.fn(() => secondDone.resolve()) const thirdStop = vi.fn(() => thirdDone.resolve()) const browserScreencast = vi .fn() .mockImplementationOnce(() => firstStart.promise) .mockResolvedValueOnce({ subscriptionId: 'browser-screencast:page-1:second', ready: { type: 'ready', subscriptionId: 'browser-screencast:page-1:second', browserPageId: 'page-1', format: 'jpeg', tab: { browserPageId: 'page-1', index: 0, url: 'about:blank', title: 'Browser', active: true } }, session: { stop: secondStop, done: secondDone.promise } }) .mockResolvedValueOnce({ subscriptionId: 'browser-screencast:page-1:third', ready: { type: 'ready', subscriptionId: 'browser-screencast:page-1:third', browserPageId: 'page-1', format: 'jpeg', tab: { browserPageId: 'page-1', index: 0, url: 'about:blank', title: 'Browser', active: true } }, session: { stop: thirdStop, done: thirdDone.promise } }) ;( runtime as unknown as { browserCommands: { browserScreencast: typeof browserScreencast } } ).browserCommands = { browserScreencast } const firstEmit = vi.fn() const secondEmit = vi.fn() const first = runtime.browserScreencast( { worktree: `id:${TEST_WORKTREE_ID}`, page: 'page-1', format: 'jpeg' }, { connectionId: 'conn-1', sendBinary: vi.fn(), emit: firstEmit } ) await Promise.resolve() const second = runtime.browserScreencast( { worktree: `id:${TEST_WORKTREE_ID}`, page: 'page-1', format: 'jpeg' }, { connectionId: 'conn-1', sendBinary: vi.fn(), emit: secondEmit } ) const thirdEmit = vi.fn() const third = runtime.browserScreencast( { worktree: `id:${TEST_WORKTREE_ID}`, page: 'page-1', format: 'jpeg' }, { connectionId: 'conn-1', sendBinary: vi.fn(), emit: thirdEmit } ) await Promise.resolve() expect(browserScreencast).toHaveBeenCalledTimes(1) firstStart.resolve({ subscriptionId: 'browser-screencast:page-1:first', ready: {} as never, session: { stop: firstStop, done: firstDone.promise } }) await first await Promise.resolve() expect(firstStop).toHaveBeenCalledTimes(1) expect(firstEmit).not.toHaveBeenCalled() expect(browserScreencast).toHaveBeenCalledTimes(2) await second await Promise.resolve() expect(secondStop).toHaveBeenCalledTimes(1) expect(browserScreencast).toHaveBeenCalledTimes(3) expect(thirdEmit).toHaveBeenCalledWith( expect.objectContaining({ subscriptionId: 'browser-screencast:page-1:third' }) ) runtime.cleanupSubscription('browser-screencast:page-1:third') await third expect(thirdStop).toHaveBeenCalledTimes(1) }) it('cancels an active same-page browser screencast before another connection starts', async () => { const runtime = createRuntime() const firstDone = deferred() const secondDone = deferred() const firstStop = vi.fn(() => firstDone.resolve()) const secondStop = vi.fn(() => secondDone.resolve()) const browserScreencast = vi .fn() .mockResolvedValueOnce({ subscriptionId: 'browser-screencast:page-1:first', ready: { type: 'ready', subscriptionId: 'browser-screencast:page-1:first', browserPageId: 'page-1', format: 'jpeg', tab: { browserPageId: 'page-1', index: 0, url: 'about:blank', title: 'Browser', active: true } }, session: { stop: firstStop, done: firstDone.promise } }) .mockResolvedValueOnce({ subscriptionId: 'browser-screencast:page-1:second', ready: { type: 'ready', subscriptionId: 'browser-screencast:page-1:second', browserPageId: 'page-1', format: 'jpeg', tab: { browserPageId: 'page-1', index: 0, url: 'about:blank', title: 'Browser', active: true } }, session: { stop: secondStop, done: secondDone.promise } }) ;( runtime as unknown as { browserCommands: { browserScreencast: typeof browserScreencast } } ).browserCommands = { browserScreencast } const firstEmit = vi.fn() const first = runtime.browserScreencast( { worktree: `id:${TEST_WORKTREE_ID}`, page: 'page-1', format: 'jpeg' }, { connectionId: 'conn-1', sendBinary: vi.fn(), emit: firstEmit } ) await vi.waitFor(() => expect(firstEmit).toHaveBeenCalledWith( expect.objectContaining({ subscriptionId: 'browser-screencast:page-1:first' }) ) ) const secondEmit = vi.fn() const second = runtime.browserScreencast( { worktree: `id:${TEST_WORKTREE_ID}`, page: 'page-1', format: 'jpeg' }, { connectionId: 'conn-2', sendBinary: vi.fn(), emit: secondEmit } ) await vi.waitFor(() => expect(firstStop).toHaveBeenCalledTimes(1)) await first await vi.waitFor(() => expect(secondEmit).toHaveBeenCalledWith( expect.objectContaining({ subscriptionId: 'browser-screencast:page-1:second' }) ) ) expect(browserScreencast).toHaveBeenCalledTimes(2) runtime.cleanupSubscription('browser-screencast:page-1:second') await second expect(secondStop).toHaveBeenCalledTimes(1) }) it('dedupes async subscription cleanup and retains a failed cleanup for retry', async () => { const runtime = createRuntime() const cleanupError = new Error('physical teardown incomplete') const firstCleanup = deferred() const cleanup = vi .fn() .mockReturnValueOnce(firstCleanup.promise) .mockRejectedValue(cleanupError) runtime.registerSubscriptionCleanup('files-watch-1', cleanup, 'conn-1') const first = runtime.cleanupSubscriptionAndWait('files-watch-1') const duplicate = runtime.cleanupSubscriptionAndWait('files-watch-1') expect(cleanup).toHaveBeenCalledTimes(1) firstCleanup.reject(cleanupError) await expect(first).rejects.toBe(cleanupError) await expect(duplicate).rejects.toBe(cleanupError) await expect(runtime.cleanupSubscriptionAndWait('files-watch-1')).rejects.toBe(cleanupError) expect(cleanup).toHaveBeenCalledTimes(2) }) it('does not let an old connection cleanup tear down its replacement subscription', async () => { const runtime = createRuntime() const oldDone = deferred() const oldCleanup = vi.fn(() => oldDone.promise) const replacementCleanup = vi.fn() runtime.registerSubscriptionCleanup('terminal:stable', oldCleanup, 'conn-old') runtime.registerSubscriptionCleanup('terminal:stable', replacementCleanup, 'conn-new') expect(oldCleanup).toHaveBeenCalledTimes(1) runtime.cleanupSubscriptionsForConnection('conn-old') expect(replacementCleanup).not.toHaveBeenCalled() runtime.cleanupSubscriptionsForConnection('conn-new') expect(replacementCleanup).toHaveBeenCalledTimes(1) oldDone.resolve() await Promise.resolve() }) it('does not let a delayed cleanup retry tear down its replacement subscription', async () => { const runtime = createRuntime() const physicalExit = deferred() const oldDone = deferred() const cleanupError = new Error('physical teardown incomplete') const oldCleanup = vi.fn(() => oldDone.promise) const replacementCleanup = vi.fn() runtime.registerSubscriptionCleanup('files-watch:stable', oldCleanup, 'conn-old') const oldAttempt = runtime.cleanupSubscriptionAndWait('files-watch:stable') runtime.retrySubscriptionCleanupAfter('files-watch:stable', oldCleanup, physicalExit.promise) runtime.registerSubscriptionCleanup('files-watch:stable', replacementCleanup, 'conn-new') oldDone.reject(cleanupError) await expect(oldAttempt).rejects.toBe(cleanupError) physicalExit.resolve() await new Promise((resolve) => setTimeout(resolve, 0)) expect(oldCleanup).toHaveBeenCalledTimes(1) expect(replacementCleanup).not.toHaveBeenCalled() await runtime.cleanupSubscriptionAndWait('files-watch:stable') expect(replacementCleanup).toHaveBeenCalledTimes(1) }) it('does not deliver or accept browser screencast frames before ready', async () => { const runtime = createRuntime() const done = deferred() const stop = vi.fn(() => done.resolve()) const startupFrame = new Uint8Array([1, 2, 3]) const sendBinary = vi.fn() const emit = vi.fn() const browserScreencast = vi.fn( async (_params: unknown, stream: { sendBinary: typeof sendBinary }) => { expect(stream.sendBinary(startupFrame)).toBe(false) expect(sendBinary).not.toHaveBeenCalled() return { subscriptionId: 'browser-screencast:page-1:first', ready: { type: 'ready', subscriptionId: 'browser-screencast:page-1:first', browserPageId: 'page-1', format: 'jpeg', tab: { browserPageId: 'page-1', index: 0, url: 'about:blank', title: 'Browser', active: true } }, session: { stop, done: done.promise } } } ) ;( runtime as unknown as { browserCommands: { browserScreencast: typeof browserScreencast } } ).browserCommands = { browserScreencast } const task = runtime.browserScreencast( { worktree: `id:${TEST_WORKTREE_ID}`, page: 'page-1', format: 'jpeg' }, { connectionId: 'conn-1', sendBinary, emit } ) await vi.waitFor(() => expect(emit).toHaveBeenCalledWith(expect.objectContaining({ type: 'ready' })) ) expect(sendBinary).not.toHaveBeenCalled() runtime.cleanupSubscription('browser-screencast:page-1:first') await task }) it('keeps already-idle status after tui-idle wait for immediate message delivery', async () => { const runtime = new OrcaRuntimeService(store) const db = new InMemoryOrchestrationMessages() const write = vi.fn().mockReturnValue(true) setInMemoryOrchestrationMessages(runtime, 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('does not resolve type-filtered message waiters for unrelated message types', async () => { const runtime = new OrcaRuntimeService(store) const waitPromise = runtime.waitForMessage('term_abc', { typeFilter: ['worker_done', 'escalation'], timeoutMs: 5000 }) let settled = false void waitPromise.then(() => { settled = true }) runtime.notifyMessageArrived('term_abc', 'heartbeat') await Promise.resolve() expect(settled).toBe(false) runtime.notifyMessageArrived('term_abc', 'worker_done') await waitPromise expect(settled).toBe(true) }) it('removes message waiter abort listeners after message arrival', async () => { const runtime = new OrcaRuntimeService(store) const controller = new AbortController() const removeListenerSpy = vi.spyOn(controller.signal, 'removeEventListener') const waitPromise = runtime.waitForMessage('term_abc', { timeoutMs: 5000, signal: controller.signal }) runtime.notifyMessageArrived('term_abc') await waitPromise expect(removeListenerSpy).toHaveBeenCalledWith('abort', expect.any(Function)) }) 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('rejects leaf PTY waits when the request signal aborts', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const controller = new AbortController() const waitPromise = runtime .waitForLeafPtyId('missing-handle', 60_000, controller.signal) .then(() => 'resolved') .catch((error: Error) => error.message) controller.abort() const outcomePromise = Promise.race([ waitPromise, new Promise<'pending'>((resolve) => setTimeout(() => resolve('pending'), 0)) ]) await vi.advanceTimersByTimeAsync(0) expect(await outcomePromise).toBe('request_aborted') } finally { vi.useRealTimers() } }) 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: [ { workspaceKind: 'git', worktreeId: 'repo-1::/tmp/worktree-a', repoId: 'repo-1', hostId: 'local', terminalPlatform: process.platform, repo: 'repo', path: '/tmp/worktree-a', branch: 'feature/foo', isArchived: false, isMainWorktree: false, hasHostSidebarActivity: true, parentWorktreeId: null, childWorktreeIds: [], displayName: 'foo', workspaceStatus: 'in-progress', sortOrder: 0, linkedIssue: 123, linkedPR: null, linkedLinearIssue: null, linkedGitLabMR: null, linkedGitLabIssue: null, comment: '', isPinned: false, isActive: false, status: 'active', unread: false, liveTerminalCount: 1, hasAttachedPty: true, lastActivityAt: 0, lastOutputAt: 321, preview: 'build green', agents: [] } ], totalCount: 1, truncated: false }) }) it('reads the linked-PR state from the renderer repoId-keyed GitHub cache', async () => { // Regression: renderer keys the PR cache by repoId::branch; reading by path::branch missed every entry (muted mobile badge). const runtimeStore = { ...store, getGitHubCache: () => ({ pr: { [`${TEST_REPO_ID}::feature/foo`]: { data: { number: 42, state: 'merged' }, fetchedAt: 1 } }, issue: {} }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const { worktrees } = await runtime.getWorktreePs() const summary = worktrees.find((w) => w.worktreeId === TEST_WORKTREE_ID) expect(summary?.linkedPR).toEqual({ number: 42, state: 'merged' }) }) it('carries persisted worktree host ownership in mobile summaries', async () => { const metaById = { [TEST_WORKTREE_ID]: { ...store.getAllWorktreeMeta()[TEST_WORKTREE_ID], hostId: 'runtime:owner-runtime' as const } } const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId] } const runtime = new OrcaRuntimeService(runtimeStore as never) const { worktrees } = await runtime.getWorktreePs() expect(worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID)).toMatchObject({ repoId: TEST_REPO_ID, hostId: 'runtime:owner-runtime' }) }) it('emits only instance- and boundary-validated lineage parents in mobile summaries', async () => { // Regression: shipped mobile clients trust parentWorktreeId blindly, so worktree.ps must not emit stale same-path lineage. const parentPath = join(tmpdir(), 'worktree-parent') const validChildPath = join(tmpdir(), 'worktree-child-valid') const staleChildPath = join(tmpdir(), 'worktree-child-stale') const crossHostChildPath = join(tmpdir(), 'worktree-child-cross-host') const parentId = `${TEST_REPO_ID}::${parentPath}` const validChildId = `${TEST_REPO_ID}::${validChildPath}` const staleChildId = `${TEST_REPO_ID}::${staleChildPath}` const crossHostChildId = `${TEST_REPO_ID}::${crossHostChildPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance', hostId: 'local', projectId: 'project-a' }), [validChildId]: makeWorktreeMeta({ instanceId: 'child-instance', hostId: 'local', projectId: 'project-a' }), // The stale child path was reused by a replacement checkout. [staleChildId]: makeWorktreeMeta({ instanceId: 'replacement-instance' }), [crossHostChildId]: makeWorktreeMeta({ instanceId: 'cross-host-child-instance', hostId: 'runtime:other-host', projectId: 'project-a' }) } const makeLineage = (childId: string, worktreeInstanceId: string): WorktreeLineage => ({ worktreeId: childId, worktreeInstanceId, parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 1 }) const lineageById: Record = { [validChildId]: makeLineage(validChildId, 'child-instance'), [staleChildId]: makeLineage(staleChildId, 'old-child-instance'), [crossHostChildId]: makeLineage(crossHostChildId, 'cross-host-child-instance') } const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, getAllWorktreeLineage: () => lineageById, getWorktreeLineage: (worktreeId: string) => lineageById[worktreeId] } vi.mocked(listWorktrees).mockResolvedValue( [parentPath, validChildPath, staleChildPath, crossHostChildPath].map((path) => ({ path, head: 'abc', branch: `feature/${basename(path)}`, isBare: false, isMainWorktree: false })) ) const runtime = new OrcaRuntimeService(runtimeStore as never) const { worktrees } = await runtime.getWorktreePs() expect(worktrees.find((worktree) => worktree.worktreeId === validChildId)).toMatchObject({ parentWorktreeId: parentId, worktreeInstanceId: 'child-instance', lineageWorktreeInstanceId: 'child-instance', parentWorktreeInstanceId: 'parent-instance' }) const staleSummary = worktrees.find((worktree) => worktree.worktreeId === staleChildId) expect(staleSummary).toMatchObject({ parentWorktreeId: null, worktreeInstanceId: 'replacement-instance' }) expect(staleSummary?.lineageWorktreeInstanceId).toBeUndefined() expect(staleSummary?.parentWorktreeInstanceId).toBeUndefined() const crossHostSummary = worktrees.find((worktree) => worktree.worktreeId === crossHostChildId) expect(crossHostSummary).toMatchObject({ parentWorktreeId: null, worktreeInstanceId: 'cross-host-child-instance' }) expect(crossHostSummary?.lineageWorktreeInstanceId).toBeUndefined() expect(crossHostSummary?.parentWorktreeInstanceId).toBeUndefined() expect(worktrees.find((worktree) => worktree.worktreeId === parentId)).toMatchObject({ childWorktreeIds: [validChildId] }) }) it('resolves WSL platforms only for repos represented in mobile summaries', async () => { await withPlatform('win32', async () => { const primaryRepo = store.getRepos()[0]! let repos = [ primaryRepo, ...Array.from({ length: 100 }, (_, index) => ({ ...primaryRepo, id: `repo-represented-${index}`, path: `C:\\repo-represented-${index}`, displayName: `repo-represented-${index}` })) ] const getProjects = vi.fn(() => repos.slice(0, 101).map((repo, index) => ({ id: `project-${index}`, displayName: repo.displayName, badgeColor: 'blue', sourceRepoIds: [repo.id], localWindowsRuntimePreference: index === 0 ? ({ kind: 'wsl' as const, distro: 'Ubuntu' } as const) : ({ kind: 'windows-host' as const } as const), createdAt: 0, updatedAt: 0 })) ) const getSettings = vi.fn(() => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' as const } })) const runtime = new OrcaRuntimeService({ ...store, getRepos: () => repos, getProjects, getSettings } as never) const { worktrees } = await runtime.getWorktreePs() expect(worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID)).toMatchObject({ repoId: TEST_REPO_ID, terminalPlatform: 'linux' }) expect(getProjects).toHaveBeenCalledTimes(1) expect(getSettings).toHaveBeenCalledTimes(2) getProjects.mockClear() getSettings.mockClear() repos = [ ...repos, ...Array.from({ length: 2_000 }, (_, index) => ({ ...repos[0]!, id: `repo-unresolved-${index}`, path: `C:\\repo-unresolved-${index}`, displayName: `repo-unresolved-${index}` })) ] await runtime.getWorktreePs() // Why: the cache already owns the batch-resolved platforms; newly persisted repos must not trigger another scan. expect(getProjects).not.toHaveBeenCalled() expect(getSettings).toHaveBeenCalledTimes(1) }) }) it('keeps each worktree poll paired with its platform generation', async () => { await withPlatform('win32', async () => { let runtimePreference: { kind: 'wsl'; distro: string } | { kind: 'windows-host' } = { kind: 'wsl', distro: 'Ubuntu' } const runtimeStore = { ...store, getProjects: () => [ { id: 'project-generation', displayName: 'generation', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: runtimePreference, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' as const } }) } const staleScan = deferred() vi.mocked(listWorktrees) .mockImplementationOnce(() => staleScan.promise) .mockResolvedValueOnce(MOCK_GIT_WORKTREES) const runtime = new OrcaRuntimeService(runtimeStore as never) const stalePoll = runtime.getWorktreePs() runtimePreference = { kind: 'windows-host' } runtime.notifyBranchRenamed(TEST_REPO_ID) const freshPoll = await runtime.getWorktreePs() staleScan.resolve(MOCK_GIT_WORKTREES) const staleResult = await stalePoll // Why: invalidation can let a newer scan finish first; each result keeps the platform map from its own generation. expect(staleResult.worktrees[0]).toMatchObject({ terminalPlatform: 'linux' }) expect(freshPoll.worktrees[0]).toMatchObject({ terminalPlatform: 'win32' }) }) }) it('omits worktrees hidden by the host visibility policy from mobile summaries', async () => { const hiddenExternalRepo = { ...store.getRepos()[0], externalWorktreeVisibility: 'hide' as const, externalWorktreeVisibilityLegacy: false } const runtimeStore = { ...store, getRepos: () => [hiddenExternalRepo], getRepo: () => hiddenExternalRepo } const runtime = new OrcaRuntimeService(runtimeStore as never) await expect(runtime.getWorktreePs()).resolves.toMatchObject({ worktrees: [], totalCount: 0, truncated: false }) }) it('marks saved session tabs with live PTYs as host sidebar activity', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.registerPty('persisted-pty', TEST_WORKTREE_ID) runtime.onPtyData('persisted-pty', 'ready\n', 456) const { worktrees } = await runtime.getWorktreePs() expect(worktrees[0]).toMatchObject({ worktreeId: TEST_WORKTREE_ID, hasHostSidebarActivity: true, status: 'active', liveTerminalCount: 1 }) }) it('attributes live legacy PTYs from saved layout bindings when their panes are hidden', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({ ...session, tabsByWorktree: { [TEST_WORKTREE_ID]: session.tabsByWorktree[TEST_WORKTREE_ID]!.map((tab) => ({ ...tab, ptyId: null })) } }) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: vi.fn(() => true), kill: vi.fn(() => true), getForegroundProcess: vi.fn(async () => null), // Legacy local PTYs have opaque ids and the local provider cannot recover cwd. listProcesses: vi.fn(async () => [{ id: 'persisted-pty', cwd: '', title: 'shell' }]) }) const { worktrees } = await runtime.getWorktreePs() expect(worktrees[0]).toMatchObject({ worktreeId: TEST_WORKTREE_ID, hasHostSidebarActivity: true, hasAttachedPty: true, liveTerminalCount: 1 }) }) it('prefers migrated layout ownership over a worktree id frozen in the PTY id', async () => { const priorWorktreeId = `${TEST_REPO_ID}::/tmp/worktree-before-rename` const migratedPtyId = `${priorWorktreeId}@@daemon-controller-pty` const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({ ...session, tabsByWorktree: { [TEST_WORKTREE_ID]: session.tabsByWorktree[TEST_WORKTREE_ID]!.map((tab) => ({ ...tab, ptyId: null })) }, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: migratedPtyId }) } }) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: vi.fn(() => true), kill: vi.fn(() => true), getForegroundProcess: vi.fn(async () => null), listProcesses: vi.fn(async () => [{ id: migratedPtyId, cwd: '', title: 'shell' }]) }) const { worktrees } = await runtime.getWorktreePs() expect(worktrees[0]).toMatchObject({ worktreeId: TEST_WORKTREE_ID, hasHostSidebarActivity: true, hasAttachedPty: true, liveTerminalCount: 1 }) }) it('does not project persisted wake identifiers as live terminal activity', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal({ activeWorktreeIdsOnShutdown: [TEST_WORKTREE_ID] }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) runtime.setPtyController({ write: vi.fn(() => true), kill: vi.fn(() => true), getForegroundProcess: vi.fn(async () => null), listProcesses: vi.fn(async () => []) }) const { worktrees } = await runtime.getWorktreePs() expect(worktrees[0]).toMatchObject({ worktreeId: TEST_WORKTREE_ID, hasHostSidebarActivity: false, liveTerminalCount: 0, hasAttachedPty: false, status: 'inactive' }) }) it('projects zero after sleep despite stale renderer leaves and persisted tabs', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session) const runtime = new OrcaRuntimeService(runtimeStore as never) const processLists = [ [{ id: 'persisted-pty', cwd: TEST_WORKTREE_PATH, title: 'Shell' }], [], [], [] ] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) runtime.attachWindow(1) const publishStaleGraph = (): void => { runtime.syncWindowGraph(1, { tabs: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, title: 'Shell', activeLeafId: HEADLESS_LEAF_ID, layout: null } ], leaves: [ { tabId: 'host-tab', worktreeId: TEST_WORKTREE_ID, leafId: HEADLESS_LEAF_ID, paneRuntimeId: 1, ptyId: 'persisted-pty' } ] }) } publishStaleGraph() await runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) // Why: another connected client can republish a pre-sleep graph after physical teardown. publishStaleGraph() const firstObserver = await runtime.getWorktreePs() const secondObserver = await runtime.getWorktreePs() for (const result of [firstObserver, secondObserver]) { expect(result.worktrees[0]).toMatchObject({ worktreeId: TEST_WORKTREE_ID, liveTerminalCount: 0, hasAttachedPty: false, status: 'inactive' }) } }) it('marks saved browser tabs as host sidebar activity like desktop', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal({ tabsByWorktree: {}, terminalLayoutsByTabId: {}, browserTabsByWorktree: { [TEST_WORKTREE_ID]: [ { id: 'browser-1', worktreeId: TEST_WORKTREE_ID, url: 'https://example.com', title: 'Example', loading: false, faviconUrl: null, canGoBack: false, canGoForward: false, loadError: null, createdAt: 1 } ] } }) ) const runtime = new OrcaRuntimeService(runtimeStore as never) const { worktrees } = await runtime.getWorktreePs() expect(worktrees[0]).toMatchObject({ worktreeId: TEST_WORKTREE_ID, hasHostSidebarActivity: true }) }) it('falls back to the path-keyed GitHub cache entry', async () => { const runtimeStore = { ...store, getGitHubCache: () => ({ pr: { [`${TEST_REPO_PATH}::feature/foo`]: { data: { number: 7, state: 'open' }, fetchedAt: 1 } }, issue: {} }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const { worktrees } = await runtime.getWorktreePs() const summary = worktrees.find((w) => w.worktreeId === TEST_WORKTREE_ID) expect(summary?.linkedPR).toEqual({ number: 7, state: 'open' }) }) it('includes folder workspaces in compact worktree summaries for mobile', async () => { const folderWorkspace = makeFolderWorkspace({ name: 'GG', comment: 'dujiao-next-eval' }) const projectGroup = makeFolderProjectGroup({ name: 'Store' }) const runtime = new OrcaRuntimeService( createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never ) const { worktrees } = await runtime.getWorktreePs() const folderSummary = worktrees.find( (worktree) => worktree.worktreeId === TEST_FOLDER_WORKSPACE_KEY ) expect(folderSummary).toMatchObject({ workspaceKind: 'folder-workspace', worktreeId: TEST_FOLDER_WORKSPACE_KEY, repoId: `folder-workspace:${TEST_FOLDER_PROJECT_GROUP_ID}`, repo: 'Store', path: TEST_FOLDER_WORKSPACE_PATH, branch: '', isArchived: false, isMainWorktree: false, hasHostSidebarActivity: false, displayName: 'GG', comment: 'dujiao-next-eval', isPinned: false, unread: false, liveTerminalCount: 0, hasAttachedPty: false, status: 'inactive' }) }) it('attaches inline agent rows from the latest OSC 9999 status', async () => { const runtime = new OrcaRuntimeService(store) const leafId = '22222222-2222-4222-8222-222222222222' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData( 'pty-1', '\x1b]9999;{"state":"working","prompt":"ship it","agentType":"codex","lastAssistantMessage":"on it"}\x07', 321 ) const { worktrees } = await runtime.getWorktreePs() const summary = worktrees.find((w) => w.worktreeId === TEST_WORKTREE_ID) expect(summary?.agents).toEqual([ expect.objectContaining({ paneKey: `tab-1:${leafId}`, parentPaneKey: null, state: 'working', agentType: 'codex', prompt: 'ship it', lastAssistantMessage: 'on it', interrupted: false, stateStartedAt: expect.any(Number), updatedAt: expect.any(Number) }) ]) }) it('attaches inline agent rows from hook-reported status (not just OSC)', async () => { // Why: agent status arrives via hooks, not OSC; worktree.ps reads the hook snapshot so mobile surfaces those agents. const leafId = '33333333-3333-4333-8333-333333333333' const paneKey = `tab-1:${leafId}` const now = Date.now() const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, worktreeId: TEST_WORKTREE_ID, tabId: 'tab-1', state: 'working', prompt: 'ship it', agentType: 'claude', lastAssistantMessage: 'on it', connectionId: null, receivedAt: now, stateStartedAt: now - 100 } ] }) const workerHandle = runtime.preAllocateHandleForPty('pty-1') runtime.setOrchestrationDb({ getActiveDispatchForTerminal: vi.fn((handle: string) => handle === workerHandle ? { id: 'ctx-1', task_id: 'task-1', assignee_handle: workerHandle, status: 'dispatched' } : undefined ), getLatestDispatchForTerminal: vi.fn(() => undefined), getTask: vi.fn(() => ({ id: 'task-1', task_title: 'Dispatch prompt work', display_name: 'Review dispatch prompts and make worker labels distinct', spec: 'Review dispatch prompts\n\nand make worker labels distinct' })), getActiveCoordinatorRun: vi.fn(() => undefined) } as never) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) const { worktrees } = await runtime.getWorktreePs() const summary = worktrees.find((w) => w.worktreeId === TEST_WORKTREE_ID) expect(summary?.agents).toEqual([ expect.objectContaining({ paneKey, state: 'working', agentType: 'claude', prompt: 'ship it', taskTitle: 'Dispatch prompt work', displayName: 'Review dispatch prompts and make worker labels distinct', lastAssistantMessage: 'on it', stateStartedAt: now - 100, updatedAt: now }) ]) expect(summary).toMatchObject({ hasHostSidebarActivity: true, status: 'working' }) }) it('uses mirrored tab ownership after a workspace rename instead of stale hook attribution', async () => { const renamedPath = '/tmp/worktree-renamed' const renamedWorktreeId = `${TEST_REPO_ID}::${renamedPath}` vi.mocked(listWorktrees).mockResolvedValue([ ...MOCK_GIT_WORKTREES, { path: renamedPath, head: 'def', branch: 'feature/renamed', isBare: false, isMainWorktree: false } ]) const metaById = { ...store.getAllWorktreeMeta(), [renamedWorktreeId]: makeWorktreeMeta({ displayName: 'renamed' }) } const session = makeWorkspaceSessionWithHeadlessTerminal({ activeWorktreeId: renamedWorktreeId, activeTabIdByWorktree: { [renamedWorktreeId]: 'host-tab' }, tabsByWorktree: { [renamedWorktreeId]: [ { id: 'host-tab', ptyId: null, worktreeId: renamedWorktreeId, title: 'Codex', customTitle: null, color: null, sortOrder: 0, createdAt: 1 } ] } }) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], getWorkspaceSession: () => session } const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => [ { paneKey: `host-tab:${HEADLESS_LEAF_ID}`, worktreeId: TEST_WORKTREE_ID, state: 'working', prompt: 'continue after rename', agentType: 'codex', connectionId: null, receivedAt: now, stateStartedAt: now - 100 } ] }) const { worktrees } = await runtime.getWorktreePs() const oldSummary = worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID) const renamedSummary = worktrees.find((worktree) => worktree.worktreeId === renamedWorktreeId) expect(oldSummary?.agents).toEqual([]) expect(renamedSummary).toMatchObject({ hasHostSidebarActivity: true, status: 'working', agents: [expect.objectContaining({ prompt: 'continue after rename' })] }) }) it('keeps a fresh OSC row when the cached hook row for the same pane is older', async () => { const now = Date.now() const leafId = '44444444-4444-4444-8444-444444444444' const paneKey = `tab-1:${leafId}` const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey, worktreeId: TEST_WORKTREE_ID, tabId: 'tab-1', state: 'working', prompt: 'stale hook row', agentType: 'claude', connectionId: null, receivedAt: now - AGENT_STATUS_STALE_AFTER_MS - 1, stateStartedAt: now - AGENT_STATUS_STALE_AFTER_MS - 100 } ] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Codex', activeLeafId: leafId, layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId, paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData( 'pty-1', '\x1b]9999;{"state":"working","prompt":"fresh OSC row","agentType":"codex"}\x07', 321 ) const { worktrees } = await runtime.getWorktreePs() const summary = worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID) expect(summary).toMatchObject({ hasHostSidebarActivity: true, status: 'working' }) expect(summary?.agents).toEqual([ expect.objectContaining({ paneKey, prompt: 'fresh OSC row', agentType: 'codex' }) ]) }) it.each([ ['blocked', 0, true, 'permission'], ['waiting', 0, true, 'permission'], ['done', 0, false, 'inactive'], ['working', -AGENT_STATUS_STALE_AFTER_MS - 1, false, 'inactive'] ] as const)( 'projects %s agent activity to mobile at freshness offset %s', async (state, updatedAtOffset, hasHostSidebarActivity, status) => { const now = Date.now() const runtime = new OrcaRuntimeService(store, undefined, { getAgentStatusSnapshot: () => [ { paneKey: 'tab-1:33333333-3333-4333-8333-333333333333', worktreeId: TEST_WORKTREE_ID, tabId: 'tab-1', state, prompt: 'mobile parity', agentType: 'codex', connectionId: null, receivedAt: now + updatedAtOffset, stateStartedAt: now - 100 } ] }) const { worktrees } = await runtime.getWorktreePs() expect(worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID)).toMatchObject({ hasHostSidebarActivity, status }) } ) it('marks the desktop-active worktree as isActive', async () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) const runtime = new OrcaRuntimeService(runtimeStore as never) const { worktrees } = await runtime.getWorktreePs() const active = worktrees.filter((w) => w.isActive) expect(active).toHaveLength(1) expect(active[0]?.worktreeId).toBe(TEST_WORKTREE_ID) }) it('includes SSH-backed worktrees in the mobile worktree summary', async () => { const remoteRepo = { id: 'repo-ssh', path: '/home/me/project', displayName: 'remote-vm', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const remoteWorktree = { path: '/home/me/project/.worktrees/feature-mobile', head: 'def', branch: 'refs/heads/feature/mobile', isBare: false, isMainWorktree: false } const metaById: Record = { [`${remoteRepo.id}::${remoteWorktree.path}`]: makeWorktreeMeta({ displayName: 'Remote mobile' }) } const getRepo = vi.fn((id: string) => (id === remoteRepo.id ? remoteRepo : undefined)) const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } registerSshGitProvider('ssh-1', { listWorktrees: vi.fn().mockResolvedValue([remoteWorktree]) } as never) const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => Array.from({ length: 100 }, (_, index) => ({ paneKey: `remote-tab:${String(index).padStart(8, '0')}-5555-4555-8555-555555555555`, worktreeId: `${remoteRepo.id}::${remoteWorktree.path}/`, tabId: 'remote-tab', state: 'working', prompt: 'remote agent without a PTY', agentType: 'codex', connectionId: 'ssh-1', receivedAt: now, stateStartedAt: now - 100 })) }) const summaries = await runtime.getWorktreePs() // Why: equal keys prove polled worktree.ps rows can share the per-request index instead of repeating path scans. expect(worktreePathComparison.worktreePathComparisonKey(remoteWorktree.path, 'linux')).toBe( worktreePathComparison.worktreePathComparisonKey(`${remoteWorktree.path}/`, 'linux') ) expect(summaries.worktrees).toEqual([ expect.objectContaining({ worktreeId: `${remoteRepo.id}::${remoteWorktree.path}`, repoId: remoteRepo.id, repo: 'remote-vm', path: remoteWorktree.path, displayName: 'Remote mobile', hasHostSidebarActivity: true, status: 'working', agents: expect.arrayContaining([ expect.objectContaining({ prompt: 'remote agent without a PTY', agentType: 'codex' }) ]) }) ]) expect(summaries.worktrees[0]?.agents).toHaveLength(100) expect(getRepo).toHaveBeenCalledTimes(1) }) it.each([ ['relative', 'project', 'feature\\name', 'feature/name', 'feature\\name', true], [ 'absolute', 'C:\\remote', '/remote/feature\\name', '/remote/feature/name', '/remote/feature\\name', true ], ['Windows SSH alias', 'C:\\remote', 'feature\\name', 'feature/name', 'feature/name', false] ] as const)( 'handles %s worktree paths when projecting mobile agents', async (_kind, repoPath, backslashPath, slashPath, projectedPath, includeSlashWorktree) => { setPlatform('win32') const remoteRepo = { id: 'repo-relative-ssh', path: repoPath, displayName: 'relative-vm', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-relative' } const backslashWorktree = { path: backslashPath, head: 'abc', branch: 'refs/heads/backslash', isBare: false, isMainWorktree: false } const slashWorktree = { ...backslashWorktree, path: slashPath, branch: 'refs/heads/slash' } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getAllWorktreeMeta: () => ({}), getWorktreeMeta: () => undefined } registerSshGitProvider('ssh-relative', { listWorktrees: vi .fn() .mockResolvedValue( includeSlashWorktree ? [backslashWorktree, slashWorktree] : [backslashWorktree] ) } as never) const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => Array.from({ length: 100 }, (_, index) => ({ paneKey: `relative-tab:${String(index).padStart(8, '0')}-5555-4555-8555-555555555555`, worktreeId: `${remoteRepo.id}::${projectedPath}/`, tabId: 'relative-tab', state: 'working', prompt: 'relative path agent', agentType: 'codex', connectionId: 'ssh-relative', receivedAt: now, stateStartedAt: now - 100 })) }) const summaries = await runtime.getWorktreePs() const backslashSummary = summaries.worktrees.find( (worktree) => worktree.path === backslashWorktree.path ) const slashSummary = summaries.worktrees.find( (worktree) => worktree.path === slashWorktree.path ) expect(backslashSummary).toMatchObject({ hasHostSidebarActivity: true, status: 'working' }) expect(backslashSummary?.agents).toHaveLength(100) expect(slashSummary?.agents).toEqual(includeSlashWorktree ? [] : undefined) const comparisonPlatform = repoPath.startsWith('C:') ? 'win32' : 'linux' const backslashKey = worktreePathComparison.worktreePathComparisonKey( backslashPath, comparisonPlatform ) const slashKey = worktreePathComparison.worktreePathComparisonKey( slashPath, comparisonPlatform ) expect(backslashKey === slashKey).toBe(!includeSlashWorktree) } ) it('projects 100 distinct pair-aware paths without rescanning the worktree list', async () => { const remoteRepo = { id: 'repo-pair-aware-scale', path: '/remote', displayName: 'pair-aware-scale-vm', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-pair-aware-scale' } let pathReadCount = 0 const remoteWorktrees = Array.from({ length: 100 }, (_, index) => { const path = `C:relative\\feature-${String(index).padStart(3, '0')}` return { get path() { pathReadCount += 1 return path }, head: `head-${index}`, branch: `refs/heads/feature-${index}`, isBare: false, isMainWorktree: false } }) const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getAllWorktreeMeta: () => ({}), getWorktreeMeta: () => undefined } registerSshGitProvider('ssh-pair-aware-scale', { listWorktrees: vi.fn().mockResolvedValue(remoteWorktrees) } as never) const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => remoteWorktrees.map((worktree, index) => ({ paneKey: `pair-aware-tab:${String(index).padStart(8, '0')}-9999-4999-8999-999999999999`, worktreeId: `${remoteRepo.id}::${win32.resolve(worktree.path)}`, tabId: 'pair-aware-tab', state: 'working' as const, prompt: `pair-aware agent ${index}`, agentType: 'codex', connectionId: 'ssh-pair-aware-scale', receivedAt: now, stateStartedAt: now - 100 })) }) const summaries = await runtime.getWorktreePs() expect(summaries.worktrees).toHaveLength(100) expect(summaries.worktrees.every((worktree) => worktree.agents?.length === 1)).toBe(true) // Why: property reads make the scaling assertion mutation-sensitive without a flaky wall-clock threshold. expect(pathReadCount).toBeLessThan(2_000) }) it('bounds 2000 distinct malformed path misses per mobile poll', async () => { const remoteRepo = { id: 'repo-malformed-scale', path: '/remote', displayName: 'malformed-scale-vm', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-malformed-scale' } let pathReadCount = 0 const remoteWorktrees = Array.from({ length: 2_000 }, (_, index) => { const path = `/remote/worktree-${String(index).padStart(4, '0')}` return { get path() { pathReadCount += 1 return path }, head: `head-${index}`, branch: `refs/heads/worktree-${index}`, isBare: false, isMainWorktree: false } }) const getRepo = vi.fn((id: string) => (id === remoteRepo.id ? remoteRepo : undefined)) const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo, getAllWorktreeMeta: () => ({}), getWorktreeMeta: () => undefined } registerSshGitProvider('ssh-malformed-scale', { listWorktrees: vi.fn().mockResolvedValue(remoteWorktrees) } as never) const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => Array.from({ length: 2_000 }, (_, index) => ({ paneKey: `malformed-tab:${String(index).padStart(8, '0')}-aaaa-4aaa-8aaa-aaaaaaaaaaaa`, worktreeId: `${remoteRepo.id}::relative/./missing-${String(index).padStart(4, '0')}\\leaf`, tabId: 'malformed-tab', state: 'working' as const, prompt: `missing agent ${index}`, agentType: 'codex', connectionId: 'ssh-malformed-scale', receivedAt: now, stateStartedAt: now - 100 })) }) const summaries = await runtime.getWorktreePs() expect(summaries).toMatchObject({ totalCount: 2_000, truncated: true }) expect(summaries.worktrees.every((worktree) => worktree.agents?.length === 0)).toBe(true) expect(getRepo).toHaveBeenCalledTimes(remoteWorktrees.length) // Why: the prior fallback read every worktree path per distinct miss, exceeding the 3s worktree.ps poll interval at this scale. expect(pathReadCount).toBeLessThan(40_000) }) it('caches repeated malformed path misses before normalizing them again', async () => { const remoteRepo = { id: 'repo-repeated-miss', path: '/remote', displayName: 'repeated-miss-vm', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-repeated-miss' } const remoteWorktree = { path: '/remote/existing', head: 'head-existing', branch: 'refs/heads/existing', isBare: false, isMainWorktree: false } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getAllWorktreeMeta: () => ({}), getWorktreeMeta: () => undefined } registerSshGitProvider('ssh-repeated-miss', { listWorktrees: vi.fn().mockResolvedValue([remoteWorktree]) } as never) const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => Array.from({ length: 2_000 }, (_, index) => ({ paneKey: `repeated-miss-tab:${String(index).padStart(8, '0')}-bbbb-4bbb-8bbb-bbbbbbbbbbbb`, worktreeId: `${remoteRepo.id}::relative/./missing\\leaf`, tabId: 'repeated-miss-tab', state: 'working' as const, prompt: `repeated missing agent ${index}`, agentType: 'codex', connectionId: 'ssh-repeated-miss', receivedAt: now, stateStartedAt: now - 100 })) }) const cwdSpy = vi.spyOn(process, 'cwd') try { const summaries = await runtime.getWorktreePs() expect(summaries.worktrees[0]?.agents).toEqual([]) // Why: resolving a relative comparison key consults cwd; a raw miss must do that once per poll, not per agent row. expect(cwdSpy.mock.calls.length).toBeLessThan(50) } finally { cwdSpy.mockRestore() } }) it('keeps no-PTY agent worktrees in the truncated mobile summary', async () => { const remoteRepo = { id: 'repo-truncated-ssh', path: '/remote', displayName: 'truncated-vm', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-truncated' } const targetPath = '/remote/zzz-live-agent' const remoteWorktrees = [ ...Array.from({ length: 200 }, (_, index) => ({ path: `/remote/inactive-${String(index).padStart(3, '0')}`, head: `head-${index}`, branch: `refs/heads/inactive-${index}`, isBare: false, isMainWorktree: false })), { path: targetPath, head: 'live-agent', branch: 'refs/heads/live-agent', isBare: false, isMainWorktree: false } ] const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getAllWorktreeMeta: () => ({}), getWorktreeMeta: () => undefined } registerSshGitProvider('ssh-truncated', { listWorktrees: vi.fn().mockResolvedValue(remoteWorktrees) } as never) const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => [ { paneKey: 'truncated-tab:77777777-7777-4777-8777-777777777777', worktreeId: `${remoteRepo.id}::${targetPath}/`, tabId: 'truncated-tab', state: 'working', prompt: 'live beyond the default limit', agentType: 'codex', connectionId: 'ssh-truncated', receivedAt: now, stateStartedAt: now - 100 } ] }) const summaries = await runtime.getWorktreePs() const target = summaries.worktrees.find((worktree) => worktree.path === targetPath) expect(summaries).toMatchObject({ totalCount: 201, truncated: true }) expect(summaries.worktrees).toHaveLength(200) expect(target).toMatchObject({ hasHostSidebarActivity: true, status: 'working' }) expect(target?.agents).toHaveLength(1) }) it('keeps pinned and unread worktrees when active rows fill the mobile summary limit', async () => { setPlatform('win32') const remoteRepo = { id: 'repo-pinned-limit', path: '/remote', displayName: 'pinned-limit-vm', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-pinned-limit' } const activeWorktrees = Array.from({ length: 199 }, (_, index) => ({ path: `relative/active-${String(index).padStart(3, '0')}`, head: `head-${index}`, branch: `refs/heads/active-${index}`, isBare: false, isMainWorktree: false })) const pinnedPath = 'relative/zzz-pinned' const pinnedWorktree = { path: pinnedPath, head: 'pinned', branch: 'refs/heads/pinned', isBare: false, isMainWorktree: false } const unreadPath = 'relative/zzz-unread' const unreadWorktree = { ...pinnedWorktree, path: unreadPath, head: 'unread', branch: 'refs/heads/unread' } const pinnedId = `${remoteRepo.id}::${pinnedPath}` const unreadId = `${remoteRepo.id}::${unreadPath}` const metaById: Record = { [pinnedId]: makeWorktreeMeta({ isPinned: true }), [unreadId]: makeWorktreeMeta({ isUnread: true }) } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId] } registerSshGitProvider('ssh-pinned-limit', { listWorktrees: vi.fn().mockResolvedValue([...activeWorktrees, pinnedWorktree, unreadWorktree]) } as never) const now = Date.now() const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { getAgentStatusSnapshot: () => activeWorktrees.map((worktree, index) => ({ paneKey: `active-tab:${String(index).padStart(8, '0')}-8888-4888-8888-888888888888`, worktreeId: `${remoteRepo.id}::${worktree.path.replace('relative/', 'relative/./')}`, tabId: 'active-tab', state: 'working' as const, prompt: 'active row', agentType: 'codex', connectionId: 'ssh-pinned-limit', receivedAt: now, stateStartedAt: now - 100 })) }) const summaries = await runtime.getWorktreePs() expect(summaries).toMatchObject({ totalCount: 201, truncated: true }) expect(summaries.worktrees).toHaveLength(200) expect(summaries.worktrees.find((worktree) => worktree.worktreeId === pinnedId)).toMatchObject({ isPinned: true, hasHostSidebarActivity: false }) expect(summaries.worktrees.find((worktree) => worktree.worktreeId === unreadId)).toMatchObject({ unread: true, hasHostSidebarActivity: false }) expect( worktreePathComparison.worktreePathComparisonKey(activeWorktrees[0]!.path, 'linux') ).toBe( worktreePathComparison.worktreePathComparisonKey( activeWorktrees[0]!.path.replace('relative/', 'relative/./'), 'linux' ) ) }) it('clears stale working status after the agent exits and the shell takes over the title', async () => { // Why (#1437): sticky lastAgentStatus left the spinner on 'working' after agent exit; recompute from the live OSC title each call. 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' } ] }) runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100) const working = await runtime.getWorktreePs() expect(working.worktrees[0].status).toBe('working') // Agent exits, shell title takes over — mobile must flip to 'active' like desktop's getWorktreeStatus. runtime.onPtyData('pty-1', '\x1b]0;bash\x07', 200) const afterExit = await runtime.getWorktreePs() expect(afterExit.worktrees[0].status).toBe('active') }) it('shows worktree.ps active when the current pane is the Claude agents screen', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude working' }) runtime.onPtyData('pty-1', '\x1b]0;claude working\x07', 100) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude agents' }) const summary = await runtime.getWorktreePs() expect(summary.worktrees[0].status).toBe('active') }) it('shows worktree.ps working when the current pane supersedes a Claude agents OSC title', async () => { const runtime = new OrcaRuntimeService(store) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude agents' }) runtime.onPtyData('pty-1', '\x1b]0;claude agents\x07', 100) syncSinglePty(runtime, 'pty-1', { paneTitle: 'claude working' }) const summary = await runtime.getWorktreePs() expect(summary.worktrees[0].status).toBe('working') }) 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('awaits physical PTY stop when destructive teardown supplies shared dedupe', async () => { const runtime = new OrcaRuntimeService(store) const physicalStop = makeDeferred() const kill = vi.fn(() => true) const stopAndWait = vi.fn(async () => { await physicalStop.promise return true }) runtime.setPtyController({ write: () => true, kill, stopAndWait, getForegroundProcess: async () => null }) syncSinglePty(runtime) const stopPty = vi.fn(async (_ptyId: string, stop: () => boolean | Promise) => ({ stopped: await stop(), owner: true })) const stopping = runtime.stopTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`, { stopPty }) await vi.waitFor(() => expect(stopAndWait).toHaveBeenCalledWith('pty-1')) expect(kill).not.toHaveBeenCalled() let settled = false void stopping.then(() => { settled = true }) await Promise.resolve() expect(settled).toBe(false) physicalStop.resolve() await expect(stopping).resolves.toEqual({ stopped: 1 }) }) it('passes a margin-adjusted RPC deadline into stopAndWait for destructive teardown', async () => { const runtime = new OrcaRuntimeService(store) const stopAndWait = vi.fn(async () => true) runtime.setPtyController({ write: () => true, kill: vi.fn(() => true), stopAndWait, getForegroundProcess: async () => null }) syncSinglePty(runtime) const stopPty = vi.fn(async (_ptyId: string, stop: () => boolean | Promise) => ({ stopped: await stop(), owner: true })) // Why: the runtime-graph sweep must bound the underlying shutdown/list RPCs // below the sweep deadline, or a wedged daemon trips the outer sweep deadline. const deadline = Date.now() + WORKTREE_PROCESS_SWEEP_TIMEOUT_MS await expect( runtime.stopTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`, { deadline, stopPty }) ).resolves.toEqual({ stopped: 1 }) expect(stopAndWait).toHaveBeenCalledTimes(1) const [ptyId, opts] = stopAndWait.mock.calls[0] as unknown as [ string, { deadlineMs?: number } | undefined ] expect(ptyId).toBe('pty-1') // Pin the margin: RPCs must settle WORKTREE_TEARDOWN_RPC_MARGIN_MS before the // sweep deadline so the accurate stop failure outruns the sweep-timeout error. expect(opts?.deadlineMs).toBe(deadline - WORKTREE_TEARDOWN_RPC_MARGIN_MS) }) 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('does not stop a reused PTY after the teardown deadline passes', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const killed = vi.fn(() => true) runtime.setPtyController({ write: () => true, kill: killed, 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: 'reused-pty' } ] }) let releaseListWorktrees = () => {} vi.mocked(listWorktrees).mockImplementationOnce( () => new Promise((resolve) => { releaseListWorktrees = () => resolve(MOCK_GIT_WORKTREES) }) ) const stopPromise = runtime.stopTerminalsForWorktree('branch:feature/foo', { deadline: Date.now() + 25 }) await vi.advanceTimersByTimeAsync(25) releaseListWorktrees() await expect(stopPromise).resolves.toEqual({ stopped: 0 }) expect(killed).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('sleeps every freshly discovered worktree PTY without a hydrated renderer graph', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] const processLists = [ [ { id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }, { id: 'pty-2', cwd: TEST_WORKTREE_PATH, title: 'Shell' } ], [] ] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId, opts) => { stopped.push(ptyId) expect(opts).toEqual( expect.objectContaining({ keepHistory: true, deadlineMs: expect.any(Number) }) ) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) await expect(runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)).resolves.toEqual({ stopped: 2, stoppedPtyIds: ['pty-1', 'pty-2'], livePtyIds: ['pty-1', 'pty-2'], postStopVerified: true }) expect(stopped).toEqual(['pty-1', 'pty-2']) }) it('uses provider-owned worktree identity when a PTY cwd has drifted', async () => { const runtime = new OrcaRuntimeService(store) const stopAndWait = vi.fn(async (ptyId: string) => { runtime.onPtyExit(ptyId, -1) return true }) const processLists = [ [ { id: 'opaque-pty-id', cwd: '/tmp/outside-the-worktree', title: 'Shell', worktreeId: TEST_WORKTREE_ID } ], [] ] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) await expect( runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) ).resolves.toMatchObject({ stoppedPtyIds: ['opaque-pty-id'], postStopVerified: true }) expect(stopAndWait).toHaveBeenCalledWith( 'opaque-pty-id', expect.objectContaining({ keepHistory: true, deadlineMs: expect.any(Number) }) ) }) it('sleeps a Windows-equivalent provider worktree identity after one request', async () => { const windowsPath = 'C:\\Repo\\Feature' const windowsWorktreeId = `${TEST_REPO_ID}::${windowsPath}` vi.mocked(listWorktrees).mockResolvedValue([ ...MOCK_GIT_WORKTREES, { path: windowsPath, head: 'windows-head', branch: 'feature/windows', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(store) const processLists = [ [ { id: 'windows-pty', cwd: 'C:/REPO/FEATURE', title: 'Shell', worktreeId: `${TEST_REPO_ID}::c:/repo/feature` } ], [] ] const stopAndWait = vi.fn(async (ptyId: string) => { runtime.onPtyExit(ptyId, -1) return true }) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) await expect( runtime.sleepTerminalsForWorktree(`id:${windowsWorktreeId}`) ).resolves.toMatchObject({ stoppedPtyIds: ['windows-pty'], postStopVerified: true }) expect(stopAndWait).toHaveBeenCalledWith( 'windows-pty', expect.objectContaining({ keepHistory: true, deadlineMs: expect.any(Number) }) ) }) it('prefers migrated persisted ownership over a provider worktree id frozen at spawn', async () => { const priorWorktreeId = `${TEST_REPO_ID}::/tmp/worktree-before-rename` const migratedPtyId = `${priorWorktreeId}@@daemon-controller-pty` const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({ ...session, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: migratedPtyId }) } }) const runtime = new OrcaRuntimeService(runtimeStore as never) const processLists = [ [ { id: migratedPtyId, cwd: '/tmp/outside-the-worktree', title: 'Shell', worktreeId: priorWorktreeId } ], [] ] const stopAndWait = vi.fn(async (ptyId: string) => { runtime.onPtyExit(ptyId, -1) return true }) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) await expect( runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) ).resolves.toMatchObject({ stoppedPtyIds: [migratedPtyId], postStopVerified: true }) expect(stopAndWait).toHaveBeenCalledWith( migratedPtyId, expect.objectContaining({ keepHistory: true, deadlineMs: expect.any(Number) }) ) }) it('treats an already-sleeping worktree as a verified idempotent success', async () => { const runtime = new OrcaRuntimeService(store) const stopAndWait = vi.fn(async () => true) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses: async () => [] }) const first = await runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) const retry = await runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) expect(first).toEqual({ stopped: 0, stoppedPtyIds: [], livePtyIds: [], postStopVerified: true }) expect(retry).toEqual(first) expect(stopAndWait).not.toHaveBeenCalled() }) it('fails worktree sleep closed when fresh host liveness is unavailable', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: vi.fn(async () => true), getForegroundProcess: async () => null, listProcesses: async () => { throw new Error('daemon unavailable') } }) await expect(runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)).rejects.toThrow( 'terminal_liveness_unavailable' ) }) it('surfaces physical worktree PTY stop failure', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: vi.fn(async () => false), getForegroundProcess: async () => null, listProcesses: async () => [{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }] }) await expect(runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)).rejects.toThrow( 'terminal_worktree_sleep_failed' ) }) it('stops only PTYs owned by the selected worktree', async () => { const otherWorktreePath = '/tmp/worktree-b' vi.mocked(listWorktrees).mockResolvedValue([ ...MOCK_GIT_WORKTREES, { path: otherWorktreePath, head: 'def', branch: 'feature/other', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] const otherPty = { id: 'pty-other', cwd: otherWorktreePath, title: 'Other' } const processLists = [ [{ id: 'pty-target', cwd: TEST_WORKTREE_PATH, title: 'Target' }, otherPty], [otherPty] ] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { stopped.push(ptyId) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [otherPty] }) await expect( runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) ).resolves.toMatchObject({ stoppedPtyIds: ['pty-target'], postStopVerified: true }) expect(stopped).toEqual(['pty-target']) }) it('does not stop a provider-owned foreign PTY referenced by stale target state', async () => { const otherWorktreePath = '/tmp/worktree-b' const otherWorktreeId = `${TEST_REPO_ID}::${otherWorktreePath}` vi.mocked(listWorktrees).mockResolvedValue([ ...MOCK_GIT_WORKTREES, { path: otherWorktreePath, head: 'def', branch: 'feature/other', isBare: false, isMainWorktree: false } ]) const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({ ...session, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'pty-foreign' }) } }) const runtime = new OrcaRuntimeService(runtimeStore as never) const stopAndWait = vi.fn(async () => true) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'pty-foreign', cwd: otherWorktreePath, title: 'Other', worktreeId: otherWorktreeId } ] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'stale-tab', worktreeId: TEST_WORKTREE_ID, title: 'Stale', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'stale-tab', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-foreign' } ] }) const projectedTarget = (await runtime.getWorktreePs()).worktrees.find( (worktree) => worktree.worktreeId === TEST_WORKTREE_ID ) expect(projectedTarget).toMatchObject({ liveTerminalCount: 0, hasAttachedPty: false }) await expect(runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)).resolves.toEqual({ stopped: 0, stoppedPtyIds: [], livePtyIds: [], postStopVerified: true }) expect(stopAndWait).not.toHaveBeenCalled() }) it('fails closed for an unresolved explicit foreign provider owner', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({ ...session, terminalLayoutsByTabId: { 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'opaque-foreign-pty' }) } }) const runtime = new OrcaRuntimeService(runtimeStore as never) const stopAndWait = vi.fn(async () => true) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'opaque-foreign-pty', cwd: TEST_WORKTREE_PATH, title: 'Other', worktreeId: `${TEST_REPO_ID}::/temporarily-unresolved-foreign-worktree` } ] }) await expect( runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) ).resolves.toMatchObject({ stopped: 0, postStopVerified: true }) expect(stopAndWait).not.toHaveBeenCalled() }) it('reports a PTY that remains live after acknowledged worktree sleep', async () => { const runtime = new OrcaRuntimeService(store) const liveProcess = { id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' } runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: vi.fn(async () => true), getForegroundProcess: async () => null, listProcesses: async () => [liveProcess] }) await expect(runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)).resolves.toEqual({ stopped: 1, stoppedPtyIds: ['pty-1'], livePtyIds: ['pty-1'], postStopVerified: false, postStopFailure: 'terminal_worktree_sleep_still_live', remainingLivePtyIds: ['pty-1'] }) }) it('reports unavailable post-stop liveness instead of assuming convergence', async () => { const runtime = new OrcaRuntimeService(store) const clientEvents: RuntimeClientEvent[][] = [[], []] for (const events of clientEvents) { runtime.onClientEvent((event) => events.push(event)) } let listCount = 0 runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => { listCount += 1 if (listCount === 2) { throw new Error('daemon unavailable') } return listCount === 1 ? [{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }] : [] } }) await expect(runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`)).resolves.toEqual({ stopped: 1, stoppedPtyIds: ['pty-1'], livePtyIds: ['pty-1'], postStopVerified: false, postStopFailure: 'terminal_liveness_unavailable' }) await expect( runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) ).resolves.toMatchObject({ stopped: 0, postStopVerified: true }) for (const events of clientEvents) { expect( events .filter( (event): event is Extract => event.type === 'worktreeTerminalSleepState' ) .filter((event) => event.phase === 'committed') .flatMap((event) => event.ptyIds) ).toContain('pty-1') expect( events.some( (event) => event.type === 'worktreeTerminalSleepState' && event.phase === 'cancelled' ) ).toBe(false) } }) it('coalesces two clients sleeping the same host worktree', async () => { const runtime = new OrcaRuntimeService(store) const initialInventory = deferred<{ id: string; cwd: string; title: string }[]>() const listProcesses = vi .fn() .mockImplementationOnce(() => initialInventory.promise) .mockResolvedValueOnce([]) const stopAndWait = vi.fn(async (ptyId: string) => { runtime.onPtyExit(ptyId, -1) return true }) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses }) const first = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) const second = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) initialInventory.resolve([{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }]) const [firstResult, secondResult] = await Promise.all([first, second]) expect(secondResult).toEqual(firstResult) expect(stopAndWait).toHaveBeenCalledTimes(1) expect(listProcesses).toHaveBeenCalledTimes(2) }) it('serializes a new terminal spawn behind physical sleep convergence', async () => { const runtime = new OrcaRuntimeService(store) const stop = deferred() const events: RuntimeClientEvent[] = [] runtime.onClientEvent((event) => events.push(event)) const listProcesses = vi .fn() .mockResolvedValueOnce([{ id: 'pty-before-sleep', cwd: TEST_WORKTREE_PATH, title: 'Shell' }]) .mockResolvedValueOnce([]) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { const result = await stop.promise runtime.onPtyExit(ptyId, -1) return result }, getForegroundProcess: async () => null, listProcesses }) const sleep = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) await vi.waitFor(() => expect(listProcesses).toHaveBeenCalledTimes(1)) expect(runtime.getTerminalSleepClientEventSnapshot()).toEqual([ expect.objectContaining({ phase: 'started', ptyIds: ['pty-before-sleep'] }) ]) let spawnLeaseAcquired = false const spawnLease = runtime.acquireWorktreeTerminalSpawn(TEST_WORKTREE_ID).then((release) => { spawnLeaseAcquired = true return release }) await Promise.resolve() expect(spawnLeaseAcquired).toBe(false) stop.resolve(true) await expect(sleep).resolves.toMatchObject({ postStopVerified: true }) const releaseSpawn = await spawnLease expect(spawnLeaseAcquired).toBe(true) releaseSpawn() expect(runtime.getTerminalSleepClientEventSnapshot()).toEqual([]) expect( events .filter((event) => event.type === 'worktreeTerminalSleepState') .map((event) => event.phase) ).toEqual(['started', 'committed', 'woken']) }) it('waits for an in-flight spawn before inventorying worktree sleep', async () => { const runtime = new OrcaRuntimeService(store) const listProcesses = vi.fn().mockResolvedValue([]) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: vi.fn(async () => true), getForegroundProcess: async () => null, listProcesses }) const releaseSpawn = await runtime.acquireWorktreeTerminalSpawn(TEST_WORKTREE_ID) const sleep = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) await Promise.resolve() expect(listProcesses).not.toHaveBeenCalled() releaseSpawn() await expect(sleep).resolves.toMatchObject({ stopped: 0, postStopVerified: true }) expect(listProcesses).toHaveBeenCalledTimes(1) }) it('expires while queued behind a spawn and never stops it later', async () => { vi.useFakeTimers() try { const runtime = new OrcaRuntimeService(store) const listProcesses = vi.fn().mockResolvedValue([]) const stopAndWait = vi.fn(async () => true) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses }) const releaseSpawn = await runtime.acquireWorktreeTerminalSpawn(TEST_WORKTREE_ID) const sleep = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) const rejection = expect(sleep).rejects.toThrow('terminal_worktree_sleep_timeout') await vi.advanceTimersByTimeAsync(12_001) await rejection releaseSpawn() await Promise.resolve() expect(listProcesses).not.toHaveBeenCalled() expect(stopAndWait).not.toHaveBeenCalled() const releaseNextSpawn = await runtime.acquireWorktreeTerminalSpawn(TEST_WORKTREE_ID) releaseNextSpawn() } finally { vi.useRealTimers() } }) it('releases the worktree terminal mutation when a wake client-event listener throws', async () => { const runtime = new OrcaRuntimeService(store) const secondListenerEvents: RuntimeClientEvent[] = [] // Why: a broken paired-client relay can throw synchronously while delivering the wake // notification. That must not abort the wake or (regression) leak the per-worktree terminal // mutation acquired in acquireWorktreeTerminalSpawn, or every later sleep wedges for 12s. runtime.onClientEvent((event) => { if (event.type === 'worktreeTerminalSleepState' && event.phase === 'woken') { throw new Error('relay_send_failed') } }) runtime.onClientEvent((event) => secondListenerEvents.push(event)) const processLists = [[{ id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }], [], []] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) // Sleep leaves the worktree in a 'sleeping' state so the next spawn emits the 'woken' event. await runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) // The wake acquires the mutation and emits 'woken'; a throwing subscriber must not surface. const releaseSpawn = await runtime.acquireWorktreeTerminalSpawn(TEST_WORKTREE_ID) releaseSpawn() // Isolation: the second subscriber still received the 'woken' event. expect( secondListenerEvents.some( (event) => event.type === 'worktreeTerminalSleepState' && event.phase === 'woken' ) ).toBe(true) // Regression: the mutation was released, so a subsequent sleep converges instead of throwing // terminal_worktree_sleep_timeout. await expect( runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) ).resolves.toMatchObject({ postStopVerified: true }) }) it('isolates a throwing subscriber across runtime listener fan-out', () => { const runtime = new OrcaRuntimeService(store) const delivered: number[] = [] // Why: the shared notifyRuntimeListeners guard must let sibling fan-outs (here mobile // notifications) survive a throwing subscriber, not just the client-event path. runtime.onNotificationDispatched(() => { throw new Error('subscriber_send_failed') }) runtime.onNotificationDispatched((event) => { delivered.push(event.notificationSeq ?? -1) }) expect(() => runtime.dispatchMobileNotification({ type: 'notification', source: 'test', title: 'Test', body: 'Body', worktreeId: TEST_WORKTREE_ID }) ).not.toThrow() // The second subscriber still received the event despite the first throwing. expect(delivered).toHaveLength(1) }) it('keeps the original committed disposition across an idempotent retry', async () => { const runtime = new OrcaRuntimeService(store) const events: RuntimeClientEvent[] = [] runtime.onClientEvent((event) => events.push(event)) const processLists = [ [{ id: 'pty-preserved-history', cwd: TEST_WORKTREE_PATH, title: 'Shell' }], [], [] ] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) await runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) expect(runtime.getTerminalSleepClientEventSnapshot()).toEqual([ expect.objectContaining({ phase: 'committed', ptyIds: ['pty-preserved-history'] }) ]) await runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) const releaseSpawn = await runtime.acquireWorktreeTerminalSpawn(TEST_WORKTREE_ID) releaseSpawn() const sleepEvents = events.filter((event) => event.type === 'worktreeTerminalSleepState') expect(sleepEvents.map((event) => event.phase)).toEqual(['started', 'committed', 'woken']) expect(sleepEvents.at(-1)?.ptyIds).toEqual(['pty-preserved-history']) }) it('keeps concurrent sleep coalesced until every launched stop settles', async () => { const runtime = new OrcaRuntimeService(store) const clientEvents: RuntimeClientEvent[][] = [[], []] for (const events of clientEvents) { runtime.onClientEvent((event) => events.push(event)) } const secondStop = deferred() const failedPty = { id: 'pty-fails', cwd: TEST_WORKTREE_PATH, title: 'Claude' } const listProcesses = vi .fn() .mockResolvedValueOnce([ failedPty, { id: 'pty-slow', cwd: TEST_WORKTREE_PATH, title: 'Shell' } ]) .mockResolvedValueOnce([failedPty]) .mockResolvedValueOnce([failedPty]) .mockResolvedValueOnce([]) let failedAttempts = 0 const stopAndWait = vi.fn(async (ptyId: string) => { if (ptyId === 'pty-fails') { failedAttempts += 1 if (failedAttempts === 1) { throw new Error('stop failed') } runtime.onPtyExit(ptyId, -1) return true } const stopped = await secondStop.promise runtime.onPtyExit(ptyId, -1) return stopped }) runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait, getForegroundProcess: async () => null, listProcesses }) const first = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) await vi.waitFor(() => expect(stopAndWait).toHaveBeenCalledTimes(2)) const concurrent = runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) secondStop.resolve(true) await expect(first).rejects.toThrow('terminal_worktree_sleep_failed') await expect(concurrent).rejects.toThrow('terminal_worktree_sleep_failed') expect(runtime.getTerminalSleepClientEventSnapshot()).toEqual([ expect.objectContaining({ phase: 'committed', ptyIds: ['pty-slow'] }) ]) await expect( runtime.sleepTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`) ).resolves.toMatchObject({ postStopVerified: true, stoppedPtyIds: ['pty-fails'] }) expect(stopAndWait).toHaveBeenCalledTimes(3) for (const events of clientEvents) { const sleepEvents = events.filter( (event): event is Extract => event.type === 'worktreeTerminalSleepState' ) expect( [ ...new Set( sleepEvents .filter((event) => event.phase === 'committed') .flatMap((event) => event.ptyIds) ) ].sort() ).toEqual(['pty-fails', 'pty-slow']) expect( sleepEvents.filter((event) => event.phase === 'cancelled').flatMap((event) => event.ptyIds) ).toEqual(['pty-fails']) } }) it('stops exactly the expected live PTYs for a worktree', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] const processLists = [[{ id: 'pty-1', cwd: '/tmp/worktree-a', title: 'Claude' }], []] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId, opts) => { stopped.push(ptyId) expect(opts).toEqual({ keepHistory: true }) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) 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' } ] }) await expect( runtime.stopExactTerminalsForWorktree('id:repo-1::/tmp/worktree-a', ['pty-1'], { keepHistory: true }) ).resolves.toEqual({ stopped: 1, stoppedPtyIds: ['pty-1'], livePtyIds: ['pty-1'], postStopVerified: true }) expect(stopped).toEqual(['pty-1']) }) it('reports recoverable post-stop liveness failure after exact terminal stop', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] const processLists = [ [{ id: 'pty-1', cwd: '/tmp/worktree-a', title: 'Claude' }], new Error('daemon unavailable') ] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { stopped.push(ptyId) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => { const next = processLists.shift() if (next instanceof Error) { throw next } return next ?? [] } }) 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' } ] }) await expect( runtime.stopExactTerminalsForWorktree('id:repo-1::/tmp/worktree-a', ['pty-1']) ).resolves.toEqual({ stopped: 1, stoppedPtyIds: ['pty-1'], livePtyIds: ['pty-1'], postStopVerified: false, postStopFailure: 'terminal_liveness_unavailable' }) expect(stopped).toEqual(['pty-1']) }) it('rejects exact terminal stop when async PTY stop fails', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId, opts) => { stopped.push(ptyId) expect(opts).toEqual({ keepHistory: true }) return false }, getForegroundProcess: async () => null, listProcesses: async () => [{ id: 'pty-1', cwd: '/tmp/worktree-a', title: 'Claude' }] }) 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' } ] }) await expect( runtime.stopExactTerminalsForWorktree('id:repo-1::/tmp/worktree-a', ['pty-1'], { keepHistory: true }) ).rejects.toThrow('terminal_exact_stop_failed') expect(stopped).toEqual(['pty-1']) }) it('rejects exact terminal stop when the live PTY set has extras', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { stopped.push(ptyId) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'pty-1', cwd: '/tmp/worktree-a', title: 'Claude' }, { id: 'pty-shell', cwd: '/tmp/worktree-a', title: 'Shell' } ] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Claude', activeLeafId: 'pane:1', layout: null }, { tabId: 'tab-2', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Shell', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' }, { tabId: 'tab-2', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 2, ptyId: 'pty-shell' } ] }) await expect( runtime.stopExactTerminalsForWorktree('id:repo-1::/tmp/worktree-a', ['pty-1']) ).rejects.toThrow('terminal_stop_pty_set_mismatch') expect(stopped).toEqual([]) }) it('allows target-only exact terminal stop when sibling PTYs remain live', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] const processLists = [ [ { id: 'pty-1', cwd: TEST_WORKTREE_PATH, title: 'Claude' }, { id: 'pty-shell', cwd: TEST_WORKTREE_PATH, title: 'Shell' } ], [{ id: 'pty-shell', cwd: TEST_WORKTREE_PATH, title: 'Shell' }] ] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId, opts) => { stopped.push(ptyId) expect(opts).toEqual({ keepHistory: true }) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Claude', activeLeafId: 'pane:1', layout: null }, { tabId: 'tab-2', worktreeId: TEST_WORKTREE_ID, title: 'Shell', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' }, { tabId: 'tab-2', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 2, ptyId: 'pty-shell' } ] }) await expect( runtime.stopExactTerminalsForWorktree(`id:${TEST_WORKTREE_ID}`, ['pty-1'], { keepHistory: true, targetOnly: true }) ).resolves.toEqual({ stopped: 1, stoppedPtyIds: ['pty-1'], livePtyIds: ['pty-1', 'pty-shell'], postStopVerified: true, remainingLivePtyIds: ['pty-shell'] }) expect(stopped).toEqual(['pty-1']) }) it('rejects exact terminal stop for multiple expected PTYs before stopping anything', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { stopped.push(ptyId) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => [ { id: 'pty-1', cwd: '/tmp/worktree-a', title: 'Claude' }, { id: 'pty-2', cwd: '/tmp/worktree-a', title: 'Codex' } ] }) 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' }, { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:2', paneRuntimeId: 2, ptyId: 'pty-2' } ] }) await expect( runtime.stopExactTerminalsForWorktree('id:repo-1::/tmp/worktree-a', ['pty-1', 'pty-2']) ).rejects.toThrow('terminal_exact_stop_requires_single_pty') expect(stopped).toEqual([]) }) it('uses fresh post-stop liveness instead of stale renderer leaves', async () => { const runtime = new OrcaRuntimeService(store) const stopped: string[] = [] const processLists = [[{ id: 'pty-1', cwd: '/tmp/worktree-a', title: 'Claude' }], []] runtime.setPtyController({ write: () => true, kill: () => false, stopAndWait: async (ptyId) => { stopped.push(ptyId) runtime.onPtyExit(ptyId, -1) return true }, getForegroundProcess: async () => null, listProcesses: async () => processLists.shift() ?? [] }) 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' }, { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:2', paneRuntimeId: 2, ptyId: 'stale-pty' } ] }) await expect( runtime.stopExactTerminalsForWorktree('id:repo-1::/tmp/worktree-a', ['pty-1']) ).resolves.toMatchObject({ stoppedPtyIds: ['pty-1'] }) expect(stopped).toEqual(['pty-1']) }) it('omits stale renderer leaves when fresh PTY liveness is required', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Stale', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'stale-pty' } ] }) const terminals = await runtime.listTerminals('id:repo-1::/tmp/worktree-a', undefined, { requireFreshPtyLiveness: true }) expect(terminals.terminals).toEqual([]) }) it('omits unbound renderer placeholders when fresh PTY liveness is required', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => [] }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Sleeping terminal', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: null } ] }) const terminals = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`, undefined, { requireFreshPtyLiveness: true }) expect(terminals).toMatchObject({ terminals: [], totalCount: 0 }) }) it('fails terminal listing closed when fresh PTY liveness is required and unavailable', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: () => true, kill: () => true, getForegroundProcess: async () => null, listProcesses: async () => { throw new Error('provider unavailable') } }) await expect( runtime.listTerminals('id:repo-1::/tmp/worktree-a', undefined, { requireFreshPtyLiveness: true }) ).rejects.toThrow('terminal_liveness_unavailable') }) 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 capped SSH refs for empty runtime repo searches', async () => { const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: () => remoteRepo } const provider = { exec: vi.fn().mockImplementation((argv: string[]) => { if (argv[0] === 'remote') { return Promise.resolve({ stdout: 'origin\nupstream\n', stderr: '' }) } return Promise.resolve({ stdout: [ 'refs/remotes/origin/main\0origin/main', 'refs/remotes/upstream/feature-x\0upstream/feature-x', 'refs/remotes/upstream/HEAD\0upstream/HEAD', 'refs/heads/local-only\0local-only' ].join('\n'), stderr: '' }) }) } registerSshGitProvider('ssh-1', provider as never) const runtime = new OrcaRuntimeService(runtimeStore as never) const result = await runtime.searchRepoRefs('id:remote-repo', '', 2) expect(result).toEqual({ refs: ['origin/main', 'upstream/feature-x'], refDetails: [ { refName: 'origin/main', localBranchName: 'main' }, { refName: 'upstream/feature-x', localBranchName: 'feature-x' } ], truncated: true }) expect(provider.exec).toHaveBeenCalledWith( expect.arrayContaining([ '--exclude=refs/remotes/**/HEAD', '--count=12', 'refs/heads/**/**', 'refs/heads/**/**/**', 'refs/remotes/**/**', 'refs/remotes/**/**/**' ]), '/home/user/repo' ) expect(provider.exec).toHaveBeenCalledWith(['remote'], '/home/user/repo') }) it('retries runtime SSH ref searches without --exclude for older git hosts', async () => { const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: () => remoteRepo } const provider = { exec: vi.fn().mockImplementation((argv: string[]) => { if (argv[0] === 'remote') { return Promise.resolve({ stdout: 'origin\n', stderr: '' }) } if (argv.includes('--exclude=refs/remotes/**/HEAD')) { return Promise.reject( Object.assign(new Error("unknown option `exclude'"), { stderr: "error: unknown option `exclude'" }) ) } return Promise.resolve({ stdout: [ 'refs/remotes/origin/main\0origin/main', 'refs/remotes/origin/HEAD\0origin/HEAD', 'refs/remotes/origin/feature-x\0origin/feature-x' ].join('\n'), stderr: '' }) }) } registerSshGitProvider('ssh-1', provider as never) const runtime = new OrcaRuntimeService(runtimeStore as never) const result = await runtime.searchRepoRefs('id:remote-repo', '', 1) const repeatedResult = await runtime.searchRepoRefs('id:remote-repo', '', 1) expect(result).toEqual({ refs: ['origin/main'], refDetails: [{ refName: 'origin/main', localBranchName: 'main' }], truncated: true }) expect(repeatedResult).toEqual(result) const forEachRefCalls = provider.exec.mock.calls.filter( (call) => (call[0] as string[])[0] === 'for-each-ref' ) expect(forEachRefCalls).toHaveLength(3) expect(forEachRefCalls[0][0]).toContain('--exclude=refs/remotes/**/HEAD') expect(forEachRefCalls[1][0]).not.toContain('--exclude=refs/remotes/**/HEAD') expect(forEachRefCalls[1][0]).toContain('--count=108') expect(forEachRefCalls[2][0]).not.toContain('--exclude=refs/remotes/**/HEAD') }) it('resolves SSH worktrees when manually updating lineage', async () => { const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const childId = `${remoteRepo.id}::/home/user/repo-child` const parentId = `${remoteRepo.id}::/home/user/repo-parent` const metaById: Record = { [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }), [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }) } const setWorktreeLineage = vi.fn((_worktreeId, lineage) => lineage) const listSshWorktrees = vi.fn().mockResolvedValue([ { path: '/home/user/repo-child', head: 'abc', branch: 'feature/child', isBare: false, isMainWorktree: false }, { path: '/home/user/repo-parent', head: 'def', branch: 'feature/parent', isBare: false, isMainWorktree: false } ]) getSshGitProviderMock.mockReturnValue({ listWorktrees: listSshWorktrees }) const runtimeStore = { ...store, getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getRepos: () => [remoteRepo], getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) expect(listSshWorktrees).toHaveBeenCalledWith(remoteRepo.path) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'manual' }) ) }) it('resolves SSH lineage updates from stored metadata when the scan cache misses', async () => { const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const childId = `${remoteRepo.id}::/home/user/repo-child` const parentId = `${remoteRepo.id}::/home/user/repo-parent` const metaById: Record = { [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }), [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }) } const setWorktreeLineage = vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage) const listSshWorktrees = vi.fn().mockResolvedValue([ { path: '/home/user/repo', head: 'abc', branch: 'main', isBare: false, isMainWorktree: true } ]) getSshGitProviderMock.mockReturnValue({ listWorktrees: listSshWorktrees }) const runtimeStore = { ...store, getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getRepos: () => [remoteRepo], getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'manual' }) ) }) it('does not resolve unknown SSH worktree ids from scan-miss fallback', async () => { const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const childId = `${remoteRepo.id}::/home/user/repo-child` const parentId = `${remoteRepo.id}::/home/user/repo-parent` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }) } const listSshWorktrees = vi.fn().mockResolvedValue([ { path: '/home/user/repo', head: 'abc', branch: 'main', isBare: false, isMainWorktree: true } ]) getSshGitProviderMock.mockReturnValue({ listWorktrees: listSshWorktrees }) const runtimeStore = { ...store, getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getRepos: () => [remoteRepo], getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage: vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage) } const runtime = new OrcaRuntimeService(runtimeStore as never) await expect( runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) ).rejects.toThrow('selector_not_found') }) it('rejects SSH lineage updates when Orca worktree identity is missing', async () => { const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const childId = `${remoteRepo.id}::/home/user/repo-child` const parentId = `${remoteRepo.id}::/home/user/repo-parent` const metaById: Record = {} const setWorktreeLineage = vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage) const fsProvider = { readFile: vi.fn(), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } getSshGitProviderMock.mockReturnValue({ listWorktrees: vi.fn().mockResolvedValue([ { path: '/home/user/repo-child', head: 'abc', branch: 'feature/child', isBare: false, isMainWorktree: false }, { path: '/home/user/repo-parent', head: 'def', branch: 'feature/parent', isBare: false, isMainWorktree: false } ]) }) registerSshFilesystemProvider('ssh-1', fsProvider as never) const runtimeStore = { ...store, getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getRepos: () => [remoteRepo], getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) try { await expect( runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) ).rejects.toThrow('Worktree instance identity was unavailable') } finally { unregisterSshFilesystemProvider('ssh-1') } expect(fsProvider.createDir).not.toHaveBeenCalled() expect(fsProvider.writeFile).not.toHaveBeenCalled() expect(setWorktreeLineage).not.toHaveBeenCalled() }) it('rejects local lineage updates when Orca worktree identity is missing', async () => { const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-lineage-')) const repoPath = join(tempRoot, 'repo') const childPath = join(tempRoot, 'child') const parentPath = join(tempRoot, 'parent') const repoId = 'local-repo' const childId = `${repoId}::${childPath}` const parentId = `${repoId}::${parentPath}` const metaById: Record = {} const setWorktreeLineage = vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage) const runtimeStore = { ...store, getRepo: (id: string) => id === repoId ? { id: repoId, path: repoPath, displayName: 'local', badgeColor: 'blue', addedAt: 1 } : undefined, getRepos: () => [ { id: repoId, path: repoPath, displayName: 'local', badgeColor: 'blue', addedAt: 1 } ], getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } vi.mocked(listWorktrees).mockResolvedValue([ { path: childPath, head: 'abc', branch: 'feature/child', isBare: false, isMainWorktree: false }, { path: parentPath, head: 'def', branch: 'feature/parent', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(runtimeStore as never) try { await mkdir(childPath, { recursive: true }) await expect( runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) ).rejects.toThrow('Worktree instance identity was unavailable') await expect(lstat(join(childPath, '.orca'))).rejects.toThrow() await expect(lstat(join(parentPath, '.orca'))).rejects.toThrow() expect(setWorktreeLineage).not.toHaveBeenCalled() } finally { await rm(tempRoot, { recursive: true, force: true }) } }) it('keeps workspace lineage in sync when manually reparenting a worktree', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/worktree-child' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const setWorktreeLineage = vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage) const setWorkspaceLineage = vi.fn((lineage: WorkspaceLineage) => lineage) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage, setWorkspaceLineage } vi.mocked(listWorktrees).mockResolvedValue([ { path: parentPath, head: 'abc', branch: 'feature/parent', isBare: false, isMainWorktree: false }, { path: childPath, head: 'def', branch: 'feature/child', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', capture: { source: 'manual-action', confidence: 'explicit' } }) ) expect(setWorkspaceLineage).toHaveBeenCalledWith( expect.objectContaining({ childWorkspaceKey: `worktree:${childId}`, childInstanceId: 'child-instance', parentWorkspaceKey: `worktree:${parentId}`, parentInstanceId: 'parent-instance', capture: { source: 'manual-action', confidence: 'explicit' } }) ) }) it.each([ { boundary: 'repository', childRepoId: 'repo-child', parentRepoId: 'repo-parent', childMeta: {}, parentMeta: {} }, { boundary: 'known host', childRepoId: TEST_REPO_ID, parentRepoId: TEST_REPO_ID, childMeta: { hostId: 'runtime:child-host' as const }, parentMeta: { hostId: 'runtime:parent-host' as const } }, { boundary: 'known project', childRepoId: TEST_REPO_ID, parentRepoId: TEST_REPO_ID, childMeta: { projectId: 'project-child' }, parentMeta: { projectId: 'project-parent' } } ])('rejects manual lineage writes across a $boundary boundary', async (scenario) => { const repos = [...new Set([scenario.childRepoId, scenario.parentRepoId])].map((id) => ({ id, path: join(tmpdir(), id), displayName: id, badgeColor: 'blue' as const, addedAt: 1 })) const childRepoPath = repos.find((repo) => repo.id === scenario.childRepoId)!.path const parentRepoPath = repos.find((repo) => repo.id === scenario.parentRepoId)!.path const childPath = join(childRepoPath, 'child') const parentPath = join(parentRepoPath, 'parent') const childId = `${scenario.childRepoId}::${childPath}` const parentId = `${scenario.parentRepoId}::${parentPath}` const metaById: Record = { [childId]: makeWorktreeMeta({ instanceId: 'child-instance', ...scenario.childMeta }), [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance', ...scenario.parentMeta }) } const setWorktreeLineage = vi.fn() const setWorkspaceLineage = vi.fn() const runtimeStore = { ...store, getRepos: () => repos, getRepo: (id: string) => repos.find((repo) => repo.id === id), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage, setWorkspaceLineage } vi.mocked(listWorktrees).mockImplementation(async (repoPath) => [ ...(repoPath === childRepoPath ? [makeWorktreeInfo(childPath)] : []), ...(repoPath === parentRepoPath ? [makeWorktreeInfo(parentPath)] : []) ]) const runtime = new OrcaRuntimeService(runtimeStore as never) await expect( runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) ).rejects.toThrow( 'Parent worktree must belong to the same repository, execution host, and project.' ) expect(setWorktreeLineage).not.toHaveBeenCalled() expect(setWorkspaceLineage).not.toHaveBeenCalled() }) it('clears workspace lineage when manually removing a parent', async () => { const childPath = '/tmp/worktree-child' const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const removeWorktreeLineage = vi.fn() const removeWorkspaceLineage = vi.fn() const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, removeWorktreeLineage, removeWorkspaceLineage } vi.mocked(listWorktrees).mockResolvedValue([ { path: childPath, head: 'def', branch: 'feature/child', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { noParent: true } }) expect(removeWorktreeLineage).toHaveBeenCalledWith(childId) expect(removeWorkspaceLineage).toHaveBeenCalledWith(`worktree:${childId}`) }) it('strips Orca provenance fields from runtime metadata updates', async () => { const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta } const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.updateManagedWorktreeMeta(`id:${TEST_WORKTREE_ID}`, { comment: 'keep me', orcaCreatedAt: 123, orcaCreationSource: 'runtime', orcaCreationWorkspaceLayout: { path: '/tmp', nestWorkspaces: false } }) expect(setWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, { comment: 'keep me' }) }) it('ignores stale instance-mismatched lineage when validating manual cycle repairs', async () => { const parentPath = '/tmp/worktree-a' const childPath = '/tmp/worktree-b' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'new-parent-instance' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const setWorktreeLineage = vi.fn((_worktreeId, lineage) => lineage) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getWorktreeLineage: (worktreeId: string) => worktreeId === parentId ? { worktreeId: parentId, worktreeInstanceId: 'old-parent-instance', parentWorktreeId: childId, parentWorktreeInstanceId: 'child-instance', origin: 'manual' as const, capture: { source: 'manual-action' as const, confidence: 'explicit' as const }, createdAt: 1 } : undefined, setWorktreeLineage } vi.mocked(listWorktrees).mockResolvedValue([ { path: parentPath, head: 'abc', branch: 'feature/a', isBare: false, isMainWorktree: false }, { path: childPath, head: 'def', branch: 'feature/b', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'new-parent-instance' }) ) }) it('rejects lineage updates when upgraded metadata is missing a parent instance id', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/worktree-child' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta(), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage: vi.fn((_worktreeId: string, lineage) => lineage) } vi.mocked(listWorktrees).mockResolvedValue([ { path: parentPath, head: 'abc', branch: 'feature/parent', isBare: false, isMainWorktree: false }, { path: childPath, head: 'def', branch: 'feature/child', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(runtimeStore as never) await expect( runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) ).rejects.toThrow('Worktree instance identity was unavailable') expect(runtimeStore.setWorktreeLineage).not.toHaveBeenCalled() }) it('rotates a missing parent instance during runtime selector scans before same-path reuse', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/worktree-child' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'old-parent-instance' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const lineageById: Record = { [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'old-parent-instance', origin: 'manual' as const, capture: { source: 'manual-action' as const, confidence: 'explicit' as const }, createdAt: 1 } } const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getWorktreeLineage: (worktreeId: string) => lineageById[worktreeId], getAllWorktreeLineage: () => lineageById, removeWorktreeLineage: vi.fn((worktreeId: string) => { delete lineageById[worktreeId] }), setWorktreeLineage } vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: childPath, head: 'def', branch: 'feature/child', isBare: false, isMainWorktree: false } ]) .mockResolvedValue([ { path: childPath, head: 'def', branch: 'feature/child', isBare: false, isMainWorktree: false }, { path: parentPath, head: 'abc', branch: 'feature/parent', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.showManagedWorktree(`id:${childId}`) const rotatedParentInstance = metaById[parentId].instanceId expect(rotatedParentInstance).toBeTruthy() expect(rotatedParentInstance).not.toBe('old-parent-instance') await runtime.updateManagedWorktreeMeta(`id:${childId}`, { comment: 'rescanned' }) expect(metaById[parentId].instanceId).toBe(rotatedParentInstance) await runtime.updateManagedWorktreeMeta(`id:${childId}`, { comment: 'touch' }) await runtime.updateManagedWorktreeMeta(`id:${childId}`, { lineage: { parentWorktree: `id:${parentId}` } }) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ worktreeInstanceId: 'child-instance', parentWorktreeInstanceId: rotatedParentInstance }) ) }) it('does not prune lineage when a runtime local worktree scan fails', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/worktree-child' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const lineageById: Record = { [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 1 } } const removeWorktreeLineage = vi.fn((worktreeId: string) => { delete lineageById[worktreeId] }) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: vi.fn((worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }), getAllWorktreeLineage: () => lineageById, removeWorktreeLineage } vi.mocked(listWorktrees).mockRejectedValueOnce(new Error('git unavailable')) const runtime = new OrcaRuntimeService(runtimeStore as never) await expect(runtime.showManagedWorktree(`id:${childId}`)).rejects.toThrow('selector_not_found') expect(removeWorktreeLineage).not.toHaveBeenCalled() expect(runtimeStore.setWorktreeMeta).not.toHaveBeenCalled() expect(lineageById[childId]).toBeTruthy() expect(metaById[parentId].instanceId).toBe('parent-instance') }) it('returns a non-authoritative detected list when a runtime local worktree scan fails', async () => { const removeWorktreeLineage = vi.fn() const runtimeStore = { ...store, getAllWorktreeLineage: () => ({}), removeWorktreeLineage } vi.mocked(listWorktrees).mockRejectedValueOnce(new Error('git unavailable')) const runtime = new OrcaRuntimeService(runtimeStore as never) await expect(runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)).resolves.toEqual({ repoId: TEST_REPO_ID, authoritative: false, source: 'metadata-fallback', worktrees: [] }) expect(removeWorktreeLineage).not.toHaveBeenCalled() }) it('hydrates runtime detected lists with instance-validated legacy lineage', async () => { const parentPath = join(tmpdir(), 'worktree-parent') const childPath = join(tmpdir(), 'worktree-child') const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const lineageById: Record = { [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'cli', capture: { source: 'explicit-cli-flag', confidence: 'explicit' }, createdAt: 1 } } const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], getAllWorktreeLineage: () => lineageById } as never) vi.mocked(listWorktrees).mockResolvedValue([ makeWorktreeInfo(childPath), makeWorktreeInfo(parentPath) ]) const result = await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) expect(result.worktrees).toEqual([ expect.objectContaining({ id: childId, parentWorktreeId: parentId, lineage: expect.objectContaining({ parentWorktreeInstanceId: 'parent-instance' }) }), expect.objectContaining({ id: parentId, parentWorktreeId: null, childWorktreeIds: [childId], lineage: null }) ]) }) it('hydrates folder-repo detected rows with instance-validated legacy lineage', async () => { const folderRepo = { id: 'folder-repo', path: '/workspace/folder', displayName: 'folder', badgeColor: 'blue' as const, addedAt: 1, kind: 'folder' as const } const parentId = `${folderRepo.id}::${folderRepo.path}` const childId = `${parentId}::workspace:child-instance` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const lineageById: Record = { [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'cli', capture: { source: 'explicit-cli-flag', confidence: 'explicit' }, createdAt: 1 } } const runtime = new OrcaRuntimeService({ ...store, getRepos: () => [folderRepo], getRepo: (id: string) => (id === folderRepo.id ? folderRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, getAllWorktreeLineage: () => lineageById } as never) const result = await runtime.listDetectedManagedWorktrees(`id:${folderRepo.id}`) expect(result.worktrees).toEqual([ expect.objectContaining({ id: parentId, parentWorktreeId: null, childWorktreeIds: [childId], lineage: null }), expect.objectContaining({ id: childId, parentWorktreeId: parentId, lineage: expect.objectContaining({ parentWorktreeInstanceId: 'parent-instance' }) }) ]) }) it('hides agent scratch created inside a linked checkout from runtime listings', async () => { const linkedCheckoutPath = '/tmp/worktree-a' const scratchPath = `${linkedCheckoutPath}/.claude/worktrees/agent-a04ccaaa` vi.mocked(listWorktrees).mockClear() vi.mocked(listWorktrees).mockResolvedValue([ makeWorktreeInfo(TEST_REPO_PATH), makeWorktreeInfo(linkedCheckoutPath), makeWorktreeInfo(scratchPath) ]) const runtime = createRuntime() const detected = await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) const listed = await runtime.listManagedWorktrees(`id:${TEST_REPO_ID}`) expect(detected.worktrees.find((worktree) => worktree.path === scratchPath)).toMatchObject({ ownership: 'agent-scratch', visible: false }) expect(listed.worktrees.map((worktree) => worktree.path)).toEqual([ TEST_REPO_PATH, linkedCheckoutPath ]) expect(listWorktrees).toHaveBeenCalledTimes(1) }) it('bounds repeated detected worktree scans across the reported 15-repo shape', async () => { vi.mocked(listWorktrees).mockReset() const repos = Array.from({ length: 15 }, (_, index) => ({ id: `repo-${index + 1}`, path: `/tmp/repo-${index + 1}`, displayName: `repo-${index + 1}`, badgeColor: 'blue' as const, addedAt: 1 })) const runtime = new OrcaRuntimeService({ ...store, getRepos: () => repos, getRepo: (id: string) => repos.find((repo) => repo.id === id), getAllWorktreeMeta: () => ({}), getWorktreeMeta: () => undefined } as never) vi.mocked(listWorktrees).mockImplementation(async (repoPath) => [ { path: `${repoPath}/main`, head: repoPath, branch: 'main', isBare: false, isMainWorktree: true } ]) const poll = async () => Promise.all(repos.map((repo) => runtime.listDetectedManagedWorktrees(`id:${repo.id}`))) const first = await poll() expect(listWorktrees).toHaveBeenCalledTimes(15) const second = await poll() expect(first.flatMap((result) => result.worktrees)).toHaveLength(15) expect(second.flatMap((result) => result.worktrees)).toHaveLength(15) expect(second.flatMap((result) => result.worktrees).map((worktree) => worktree.path)).toEqual( repos.map((repo) => `${repo.path}/main`) ) expect(listWorktrees).toHaveBeenCalledTimes(15) }) it('worktree scan cache: shares one in-flight repo scan across concurrent consumers', async () => { vi.mocked(listWorktrees).mockClear() const pending = deferred[]>() vi.mocked(listWorktrees).mockReturnValueOnce(pending.promise) const runtime = createRuntime() const detected = runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) const resolved = runtime.listManagedWorktrees() await Promise.resolve() expect(listWorktrees).toHaveBeenCalledTimes(1) pending.resolve([makeWorktreeInfo(TEST_WORKTREE_PATH)]) await expect(Promise.all([detected, resolved])).resolves.toBeTruthy() expect(listWorktrees).toHaveBeenCalledTimes(1) }) it('worktree scan cache: rescans immediately after worktree invalidation', async () => { vi.mocked(listWorktrees).mockClear() const runtime = createRuntime() await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) runtime.notifyBranchRenamed(TEST_REPO_ID) await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) expect(listWorktrees).toHaveBeenCalledTimes(2) }) it('worktree scan cache: repo invalidation preserves sibling raw scans', async () => { vi.mocked(listWorktrees).mockClear() const repos = [ { ...store.getRepos()[0], id: 'repo-a', path: '/tmp/repo-a' }, { ...store.getRepos()[0], id: 'repo-b', path: '/tmp/repo-b' } ] const runtime = new OrcaRuntimeService({ ...store, getRepos: () => repos, getRepo: (id: string) => repos.find((repo) => repo.id === id) } as never) vi.mocked(listWorktrees).mockImplementation(async (repoPath) => [makeWorktreeInfo(repoPath)]) await runtime.listDetectedManagedWorktrees('id:repo-a') await runtime.listDetectedManagedWorktrees('id:repo-b') runtime.notifyBranchRenamed('repo-a') await runtime.listDetectedManagedWorktrees('id:repo-b') await runtime.listDetectedManagedWorktrees('id:repo-a') expect(listWorktrees).toHaveBeenCalledTimes(3) expect(listWorktrees).toHaveBeenNthCalledWith(3, '/tmp/repo-a') }) it('worktree scan cache: metadata invalidation preserves raw scans', async () => { vi.mocked(listWorktrees).mockClear() const runtime = createRuntime() await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) runtime.notifyWorktreesChangedForRemoteClients(TEST_REPO_ID) await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) expect(listWorktrees).toHaveBeenCalledTimes(1) }) it('worktree scan cache: folder metadata invalidation preserves raw scans', async () => { vi.mocked(listWorktrees).mockClear() const runtime = createRuntime() await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) runtime.notifyFolderWorkspaceChanged() await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) expect(listWorktrees).toHaveBeenCalledTimes(1) }) it('worktree scan cache: expires scans per repo without coupling sibling repos', async () => { vi.mocked(listWorktrees).mockClear() vi.useFakeTimers({ now: 0 }) try { const repos = [ { ...store.getRepos()[0], id: 'repo-a', path: '/tmp/repo-a' }, { ...store.getRepos()[0], id: 'repo-b', path: '/tmp/repo-b' } ] const runtime = new OrcaRuntimeService({ ...store, getRepos: () => repos, getRepo: (id: string) => repos.find((repo) => repo.id === id) } as never) vi.mocked(listWorktrees).mockImplementation(async (repoPath) => [makeWorktreeInfo(repoPath)]) await runtime.listDetectedManagedWorktrees('id:repo-a') await vi.advanceTimersByTimeAsync(10_000) await runtime.listDetectedManagedWorktrees('id:repo-b') await vi.advanceTimersByTimeAsync(20_000) await runtime.listDetectedManagedWorktrees('id:repo-a') await runtime.listDetectedManagedWorktrees('id:repo-b') expect(listWorktrees).toHaveBeenCalledTimes(3) expect(listWorktrees).toHaveBeenNthCalledWith(3, '/tmp/repo-a') } finally { vi.useRealTimers() } }) it('worktree scan cache: does not cache non-authoritative scan failures', async () => { vi.mocked(listWorktrees).mockClear() const runtime = createRuntime() vi.mocked(listWorktrees) .mockRejectedValueOnce(new Error('git unavailable')) .mockResolvedValueOnce([makeWorktreeInfo(TEST_WORKTREE_PATH)]) await expect(runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)).resolves.toMatchObject( { authoritative: false } ) await expect(runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)).resolves.toMatchObject( { authoritative: true } ) expect(listWorktrees).toHaveBeenCalledTimes(2) }) it('worktree scan cache: invalidates when SSH availability changes', async () => { const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' } const remoteStore = { ...store, getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getRepos: () => [remoteRepo] } const provider = { listWorktrees: vi.fn().mockResolvedValue(MOCK_GIT_WORKTREES) } registerSshGitProvider('ssh-1', provider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await expect( runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) ).resolves.toMatchObject({ authoritative: true }) unregisterSshGitProvider('ssh-1') await expect( runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) ).resolves.toMatchObject({ authoritative: false }) expect(provider.listWorktrees).toHaveBeenCalledTimes(1) } finally { unregisterSshGitProvider('ssh-1') } }) it('worktree scan cache: SSH state changes do not evict local repo scans', async () => { vi.mocked(listWorktrees).mockClear() const localRepo = { ...store.getRepo(TEST_REPO_ID)!, id: 'local-repo' } const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, id: 'remote-repo', connectionId: 'ssh-1' } const repos = [localRepo, remoteRepo] const remoteStore = { ...store, getRepo: (id: string) => repos.find((repo) => repo.id === id), getRepos: () => repos } const provider = { listWorktrees: vi.fn().mockResolvedValue(MOCK_GIT_WORKTREES) } registerSshGitProvider('ssh-1', provider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await runtime.listDetectedManagedWorktrees('id:local-repo') await runtime.listDetectedManagedWorktrees('id:remote-repo') runtime.notifySshStateChanged('ssh-1', { targetId: 'ssh-1', status: 'disconnected', error: null, reconnectAttempt: 0 }) await runtime.listDetectedManagedWorktrees('id:local-repo') await runtime.listDetectedManagedWorktrees('id:remote-repo') expect(listWorktrees).toHaveBeenCalledTimes(1) expect(provider.listWorktrees).toHaveBeenCalledTimes(2) } finally { unregisterSshGitProvider('ssh-1') } }) it('worktree scan cache: SSH state changes invalidate empty resolved snapshots', async () => { const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-empty' } const remoteStore = { ...store, getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getRepos: () => [remoteRepo] } const provider = { listWorktrees: vi.fn().mockResolvedValue([]) } registerSshGitProvider('ssh-empty', provider as never) const runtime = new OrcaRuntimeService(remoteStore as never) try { await expect(runtime.listManagedWorktrees()).resolves.toMatchObject({ totalCount: 0 }) runtime.notifySshStateChanged('ssh-empty', { targetId: 'ssh-empty', status: 'connected', error: null, reconnectAttempt: 0 }) await expect(runtime.listManagedWorktrees()).resolves.toMatchObject({ totalCount: 0 }) expect(provider.listWorktrees).toHaveBeenCalledTimes(2) } finally { unregisterSshGitProvider('ssh-empty') } }) it('worktree scan cache: SSH state changes for unknown targets keep local snapshots in flight', async () => { vi.mocked(listWorktrees).mockClear() const pending = deferred[]>() vi.mocked(listWorktrees).mockReturnValueOnce(pending.promise) const runtime = new OrcaRuntimeService(store) const first = runtime.listManagedWorktrees() await Promise.resolve() runtime.notifySshStateChanged('ssh-unknown', { targetId: 'ssh-unknown', status: 'disconnected', error: null, reconnectAttempt: 0 }) pending.resolve([makeWorktreeInfo(TEST_WORKTREE_PATH)]) await expect(first).resolves.toMatchObject({ totalCount: 1 }) await expect(runtime.listManagedWorktrees()).resolves.toMatchObject({ totalCount: 1 }) expect(listWorktrees).toHaveBeenCalledTimes(1) }) it('worktree scan cache: separates local project runtime changes', async () => { vi.mocked(listWorktrees).mockClear() let runtimeDefault: unknown = { kind: 'windows-host' } const runtimeStore = { ...store, getProjects: () => [{ id: 'project-1', sourceRepoIds: [TEST_REPO_ID] }], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: runtimeDefault }) } await withPlatform('win32', async () => { const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) runtimeDefault = { kind: 'wsl', distro: 'Ubuntu' } await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) }) expect(listWorktrees).toHaveBeenCalledTimes(2) }) it('worktree scan cache: ignores a late result from an older runtime key', async () => { vi.mocked(listWorktrees).mockClear() let runtimeDefault: unknown = { kind: 'windows-host' } const runtimeStore = { ...store, getProjects: () => [{ id: 'project-1', sourceRepoIds: [TEST_REPO_ID] }], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: runtimeDefault }) } const firstScan = deferred[]>() const secondScan = deferred[]>() vi.mocked(listWorktrees) .mockReturnValueOnce(firstScan.promise) .mockReturnValueOnce(secondScan.promise) await withPlatform('win32', async () => { const runtime = new OrcaRuntimeService(runtimeStore as never) const first = runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) await Promise.resolve() expect(listWorktrees).toHaveBeenCalledTimes(1) runtimeDefault = { kind: 'wsl', distro: 'Ubuntu' } const second = runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) await Promise.resolve() expect(listWorktrees).toHaveBeenCalledTimes(2) secondScan.resolve([makeWorktreeInfo(TEST_WORKTREE_PATH)]) await second firstScan.resolve([makeWorktreeInfo(TEST_WORKTREE_PATH)]) await first await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) }) expect(listWorktrees).toHaveBeenCalledTimes(2) }) it('worktree scan cache: keeps lineage shaping outside the raw scan cache', async () => { vi.mocked(listWorktrees).mockClear() const paths = ['orchestration', 'cli', 'manual'] const metaById: Record = {} const lineageById: Record = {} const worktrees = paths.flatMap((origin, index) => { const parentPath = `/tmp/${origin}-parent` const childPath = `/tmp/${origin}-child` const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` metaById[parentId] = makeWorktreeMeta({ instanceId: `parent-${index}` }) metaById[childId] = makeWorktreeMeta({ instanceId: `child-${index}` }) lineageById[childId] = { worktreeId: childId, worktreeInstanceId: `child-${index}`, parentWorktreeId: parentId, parentWorktreeInstanceId: `parent-${index}`, origin: origin === 'orchestration' ? 'orchestration' : origin === 'cli' ? 'cli' : 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 1 } return [ { path: parentPath, head: 'parent', branch: origin, isBare: false, isMainWorktree: false }, { path: childPath, head: 'child', branch: `${origin}-child`, isBare: false, isMainWorktree: false } ] }) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getAllWorktreeLineage: () => lineageById } vi.mocked(listWorktrees).mockResolvedValue(worktrees) const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) const listed = await runtime.listManagedWorktrees(`id:${TEST_REPO_ID}`) expect( listed.worktrees .filter((worktree) => worktree.lineage) .map((worktree) => worktree.lineage?.origin) ).toEqual(paths) expect(listWorktrees).toHaveBeenCalledTimes(1) }) it('does not prune lineage when an SSH runtime provider is unavailable', async () => { const remoteRepo = { id: 'remote-repo', path: '/home/user/repo', displayName: 'remote', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } const parentId = `${remoteRepo.id}::/home/user/repo-parent` const childId = `${remoteRepo.id}::/home/user/repo-child` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance' }) } const lineageById: Record = { [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 1 } } const removeWorktreeLineage = vi.fn((worktreeId: string) => { delete lineageById[worktreeId] }) const runtimeStore = { ...store, getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getRepos: () => [remoteRepo], getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: vi.fn((worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }), getAllWorktreeLineage: () => lineageById, removeWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) await expect(runtime.showManagedWorktree(`id:${childId}`)).resolves.toMatchObject({ id: childId, parentWorktreeId: parentId, lineage: lineageById[childId] }) expect(removeWorktreeLineage).not.toHaveBeenCalled() expect(runtimeStore.setWorktreeMeta).not.toHaveBeenCalled() expect(lineageById[childId]).toBeTruthy() expect(metaById[parentId].instanceId).toBe('parent-instance') }) it('exposes valid parent and child lineage in CLI worktree records', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/worktree-child' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance', displayName: 'parent' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance', displayName: 'child' }) } const lineageById: Record = { [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 1 } } const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...metaById[worktreeId], ...meta } return metaById[worktreeId] }, getAllWorktreeLineage: () => lineageById } vi.mocked(listWorktrees).mockResolvedValue([ { path: parentPath, head: 'abc', branch: 'feature/parent', isBare: false, isMainWorktree: false }, { path: childPath, head: 'def', branch: 'feature/child', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(runtimeStore as never) const listed = await runtime.listManagedWorktrees('id:repo-1') const parent = listed.worktrees.find((worktree) => worktree.id === parentId) const child = listed.worktrees.find((worktree) => worktree.id === childId) expect(parent).toMatchObject({ parentWorktreeId: null, childWorktreeIds: [childId], lineage: null }) expect(child).toMatchObject({ parentWorktreeId: parentId, childWorktreeIds: [], lineage: lineageById[childId] }) await expect(runtime.showManagedWorktree(`id:${childId}`)).resolves.toMatchObject({ id: childId, parentWorktreeId: parentId, childWorktreeIds: [], lineage: lineageById[childId] }) }) it('keeps valid orchestration lineage when caller terminal context is stale', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/workspaces/worker-child' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance', displayName: 'coordinator' }) } const setWorktreeLineage = vi.fn((worktreeId: string, lineage) => { metaById[worktreeId] = metaById[worktreeId] ?? makeWorktreeMeta() return lineage }) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { const existing = metaById[worktreeId] ?? makeWorktreeMeta({ instanceId: 'child-instance' }) metaById[worktreeId] = { ...existing, ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) computeWorktreePathMock.mockReturnValue(childPath) ensurePathWithinWorkspaceMock.mockReturnValue(childPath) vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: parentPath, head: 'abc', branch: 'feature/coordinator', isBare: false, isMainWorktree: false } ]) .mockResolvedValueOnce([ { path: childPath, head: 'def', branch: 'worker-child', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'worker-child', lineage: { callerTerminalHandle: 'term_stale', orchestrationContext: { parentWorktreeId: parentId, orchestrationRunId: 'run-1', taskId: 'task-1', coordinatorHandle: 'term_coord' } } }) expect(result.lineage).toMatchObject({ worktreeId: childId, parentWorktreeId: parentId, origin: 'orchestration', capture: { source: 'orchestration-context', confidence: 'inferred' }, orchestrationRunId: 'run-1', taskId: 'task-1', coordinatorHandle: 'term_coord' }) expect(result.lineage).not.toHaveProperty('createdByTerminalHandle') expect(result.warnings).toEqual([]) expect(setWorktreeLineage).toHaveBeenCalledWith(childId, expect.any(Object)) }) it('enriches caller-terminal lineage with active orchestration dispatch context', async () => { const workerPath = '/tmp/worktree-worker' const childPath = '/tmp/workspaces/worker-child' const childId = `${TEST_REPO_ID}::${childPath}` const workerId = `${TEST_REPO_ID}::${workerPath}` const metaById: Record = { [TEST_WORKTREE_ID]: makeWorktreeMeta({ instanceId: 'parent-instance', displayName: 'coordinator' }), [workerId]: makeWorktreeMeta({ instanceId: 'worker-instance', displayName: 'worker' }) } const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { const existing = metaById[worktreeId] ?? makeWorktreeMeta() metaById[worktreeId] = { ...existing, ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) const workerHandle = runtime.preAllocateHandleForPty('pty-worker') const coordinatorHandle = runtime.preAllocateHandleForPty('pty-coordinator') runtime.setOrchestrationDb({ getActiveDispatchForTerminal: vi.fn(() => ({ task_id: 'task-1' })), getActiveCoordinatorRun: vi.fn(() => ({ id: 'run-1', coordinator_handle: coordinatorHandle })) } as never) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-worker', worktreeId: workerId, title: 'Worker', activeLeafId: 'pane:1', layout: null }, { tabId: 'tab-coordinator', worktreeId: TEST_WORKTREE_ID, title: 'Coordinator', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-worker', worktreeId: workerId, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-worker', paneTitle: null }, { tabId: 'tab-coordinator', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 2, ptyId: 'pty-coordinator', paneTitle: null } ] }) computeWorktreePathMock.mockReturnValue(childPath) ensurePathWithinWorkspaceMock.mockReturnValue(childPath) vi.mocked(listWorktrees) .mockResolvedValueOnce([ ...MOCK_GIT_WORKTREES, { path: workerPath, head: 'fed', branch: 'feature/worker', isBare: false, isMainWorktree: false } ]) .mockResolvedValueOnce([ { path: childPath, head: 'def', branch: 'worker-child', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'worker-child', lineage: { callerTerminalHandle: workerHandle } }) expect(result.lineage).toMatchObject({ worktreeId: childId, parentWorktreeId: workerId, origin: 'orchestration', capture: { source: 'orchestration-context', confidence: 'inferred' }, orchestrationRunId: 'run-1', taskId: 'task-1', coordinatorHandle, createdByTerminalHandle: workerHandle }) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ worktreeInstanceId: expect.not.stringMatching(/^old-/), parentWorktreeInstanceId: 'worker-instance' }) ) }) it('returns active orchestration context for renderer-synced terminal leaves', () => { const runtime = new OrcaRuntimeService(store) const workerLeafId = '11111111-1111-4111-8111-111111111111' const coordinatorLeafId = '22222222-2222-4222-8222-222222222222' const workerPaneKey = makePaneKey('tab-worker', workerLeafId) const coordinatorPaneKey = makePaneKey('tab-coordinator', coordinatorLeafId) const workerHandle = runtime.preAllocateHandleForPty('pty-worker') const coordinatorHandle = runtime.preAllocateHandleForPty('pty-coordinator') runtime.setOrchestrationDb({ getActiveDispatchForTerminal: vi.fn((handle: string) => handle === workerHandle ? { id: 'ctx-1', task_id: 'task-1', assignee_handle: workerHandle, status: 'dispatched' } : undefined ), getLatestDispatchForTerminal: vi.fn((handle: string) => handle === workerHandle ? { id: 'ctx-done', task_id: 'task-done', assignee_handle: workerHandle, status: 'completed', completed_at: new Date(Date.now()).toISOString() } : undefined ), getTask: vi.fn(() => ({ id: 'task-1', task_title: 'Dispatch prompt work', display_name: 'Review dispatch prompts and make worker labels distinct', spec: 'Review dispatch prompts\n\nand make worker labels distinct', created_by_terminal_handle: coordinatorHandle })), getActiveCoordinatorRun: vi.fn(() => ({ id: 'run-1', coordinator_handle: coordinatorHandle })) } as never) runtime.attachWindow(1) const result = runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, title: 'Claude Code', activeLeafId: workerLeafId, layout: null }, { tabId: 'tab-coordinator', worktreeId: TEST_WORKTREE_ID, title: 'Codex', activeLeafId: coordinatorLeafId, layout: null } ], leaves: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, leafId: workerLeafId, paneRuntimeId: 1, ptyId: 'pty-worker', paneTitle: null }, { tabId: 'tab-coordinator', worktreeId: TEST_WORKTREE_ID, leafId: coordinatorLeafId, paneRuntimeId: 2, ptyId: 'pty-coordinator', paneTitle: null } ] }) expect(result.agentOrchestrationByPaneKey?.[workerPaneKey]).toMatchObject({ taskId: 'task-1', dispatchId: 'ctx-1', taskTitle: 'Dispatch prompt work', displayName: 'Review dispatch prompts and make worker labels distinct', parentPaneKey: coordinatorPaneKey, parentTerminalHandle: coordinatorHandle, coordinatorHandle, orchestrationRunId: 'run-1' }) }) it('returns completed orchestration context for renderer-synced terminal leaves', () => { const runtime = new OrcaRuntimeService(store) const workerLeafId = '33333333-3333-4333-8333-333333333333' const coordinatorLeafId = '44444444-4444-4444-8444-444444444444' const workerPaneKey = makePaneKey('tab-worker', workerLeafId) const coordinatorPaneKey = makePaneKey('tab-coordinator', coordinatorLeafId) const workerHandle = runtime.preAllocateHandleForPty('pty-worker') const coordinatorHandle = runtime.preAllocateHandleForPty('pty-coordinator') runtime.setOrchestrationDb({ getActiveDispatchForTerminal: vi.fn(() => undefined), getLatestDispatchForTerminal: vi.fn((handle: string) => handle === workerHandle ? { id: 'ctx-done', task_id: 'task-done', assignee_handle: workerHandle, status: 'completed', completed_at: new Date(Date.now()).toISOString() } : undefined ), getTask: vi.fn(() => ({ id: 'task-done', created_by_terminal_handle: coordinatorHandle })) } as never) runtime.attachWindow(1) const result = runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, title: 'Claude Code', activeLeafId: workerLeafId, layout: null }, { tabId: 'tab-coordinator', worktreeId: TEST_WORKTREE_ID, title: 'Codex', activeLeafId: coordinatorLeafId, layout: null } ], leaves: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, leafId: workerLeafId, paneRuntimeId: 1, ptyId: 'pty-worker', paneTitle: null }, { tabId: 'tab-coordinator', worktreeId: TEST_WORKTREE_ID, leafId: coordinatorLeafId, paneRuntimeId: 2, ptyId: 'pty-coordinator', paneTitle: null } ] }) expect(result.agentOrchestrationByPaneKey?.[workerPaneKey]).toMatchObject({ taskId: 'task-done', dispatchId: 'ctx-done', parentPaneKey: coordinatorPaneKey, parentTerminalHandle: coordinatorHandle }) }) it('does not attach an unrelated active coordinator run to a completed dispatch', () => { const runtime = new OrcaRuntimeService(store) const workerLeafId = '55555555-5555-4555-8555-555555555555' const workerPaneKey = makePaneKey('tab-worker', workerLeafId) const workerHandle = runtime.preAllocateHandleForPty('pty-worker') runtime.setOrchestrationDb({ getActiveDispatchForTerminal: vi.fn(() => undefined), getLatestDispatchForTerminal: vi.fn((handle: string) => handle === workerHandle ? { id: 'ctx-done', task_id: 'task-done', assignee_handle: workerHandle, status: 'completed', completed_at: new Date(Date.now()).toISOString() } : undefined ), getTask: vi.fn(() => ({ id: 'task-done' })), getActiveCoordinatorRun: vi.fn(() => ({ id: 'run-unrelated', coordinator_handle: 'term_unrelated' })) } as never) runtime.attachWindow(1) const result = runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, title: 'Claude Code', activeLeafId: workerLeafId, layout: null } ], leaves: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, leafId: workerLeafId, paneRuntimeId: 1, ptyId: 'pty-worker', paneTitle: null } ] }) expect(result.agentOrchestrationByPaneKey?.[workerPaneKey]).toEqual({ taskId: 'task-done', dispatchId: 'ctx-done' }) }) it('does not return stale completed orchestration context for renderer-synced terminal leaves', () => { const runtime = new OrcaRuntimeService(store) const workerLeafId = '77777777-7777-4777-8777-777777777777' const workerHandle = runtime.preAllocateHandleForPty('pty-worker') runtime.setOrchestrationDb({ getActiveDispatchForTerminal: vi.fn(() => undefined), getLatestDispatchForTerminal: vi.fn((handle: string) => handle === workerHandle ? { id: 'ctx-stale', task_id: 'task-stale', assignee_handle: workerHandle, status: 'completed', completed_at: new Date(Date.now() - AGENT_STATUS_STALE_AFTER_MS - 1).toISOString() } : undefined ) } as never) runtime.attachWindow(1) const result = runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, title: 'Claude Code', activeLeafId: workerLeafId, layout: null } ], leaves: [ { tabId: 'tab-worker', worktreeId: TEST_WORKTREE_ID, leafId: workerLeafId, paneRuntimeId: 1, ptyId: 'pty-worker', paneTitle: null } ] }) expect(result.agentOrchestrationByPaneKey).toBeUndefined() }) it('falls back to cwd lineage when the caller terminal handle is stale', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/workspaces/cwd-child' const parentId = `${TEST_REPO_ID}::${parentPath}` const childId = `${TEST_REPO_ID}::${childPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }) } const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { const existing = metaById[worktreeId] ?? makeWorktreeMeta() metaById[worktreeId] = { ...existing, ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) computeWorktreePathMock.mockReturnValue(childPath) ensurePathWithinWorkspaceMock.mockReturnValue(childPath) vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: parentPath, head: 'abc', branch: 'feature/parent', isBare: false, isMainWorktree: false } ]) .mockResolvedValueOnce([ { path: childPath, head: 'def', branch: 'cwd-child', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'cwd-child', lineage: { callerTerminalHandle: 'term_stale', cwdParentWorktree: `id:${parentId}` } }) expect(result.lineage).toMatchObject({ worktreeId: childId, parentWorktreeId: parentId, origin: 'cli', capture: { source: 'cwd-context', confidence: 'inferred' } }) expect(result.worktree).toMatchObject({ parentWorktreeId: parentId, childWorktreeIds: [], lineage: result.lineage }) expect(setWorktreeLineage).toHaveBeenCalledWith(childId, expect.any(Object)) }) it('keeps cwd-inferred lineage best-effort when the cwd parent cannot be resolved', async () => { const childPath = '/tmp/workspaces/no-cwd-parent' computeWorktreePathMock.mockReturnValue(childPath) ensurePathWithinWorkspaceMock.mockReturnValue(childPath) vi.mocked(listWorktrees) .mockResolvedValueOnce(MOCK_GIT_WORKTREES) .mockResolvedValueOnce([ { path: childPath, head: 'def', branch: 'no-cwd-parent', isBare: false, isMainWorktree: false } ]) const runtime = new OrcaRuntimeService(store) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'no-cwd-parent', lineage: { cwdParentWorktree: 'id:repo-1::/tmp/missing-parent' } }) expect(result.lineage).toBeNull() expect(result.worktree).toMatchObject({ parentWorktreeId: null, childWorktreeIds: [], lineage: null }) expect(result.warnings).toEqual([ expect.objectContaining({ code: 'LINEAGE_PARENT_CONTEXT_MISSING', message: 'Worktree created, but Orca could not validate the current directory as a parent context.' }) ]) }) it('infers orchestration lineage from task-id comments when dispatch is completed', async () => { const workerPath = '/tmp/worktree-worker' const childPath = '/tmp/workspaces/worker-child' const childId = `${TEST_REPO_ID}::${childPath}` const workerId = `${TEST_REPO_ID}::${workerPath}` const metaById: Record = { [workerId]: makeWorktreeMeta({ instanceId: 'worker-instance', displayName: 'worker' }) } const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { const existing = metaById[worktreeId] ?? makeWorktreeMeta() metaById[worktreeId] = { ...existing, ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) const workerHandle = runtime.preAllocateHandleForPty('pty-worker') runtime.setOrchestrationDb({ getDispatchContext: vi.fn(() => ({ task_id: 'task_abc123', assignee_handle: workerHandle, status: 'completed' })) } as never) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-worker', worktreeId: workerId, title: 'Worker', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-worker', worktreeId: workerId, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-worker', paneTitle: null } ] }) computeWorktreePathMock.mockReturnValue(childPath) ensurePathWithinWorkspaceMock.mockReturnValue(childPath) vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: workerPath, head: 'fed', branch: 'feature/worker', isBare: false, isMainWorktree: false } ]) .mockResolvedValueOnce([ { path: childPath, head: 'def', branch: 'worker-child', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'worker-child', comment: 'Created via orchestration task task_abc123' }) expect(result.lineage).toMatchObject({ worktreeId: childId, parentWorktreeId: workerId, origin: 'orchestration', capture: { source: 'orchestration-context', confidence: 'inferred' }, taskId: 'task_abc123' }) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ parentWorktreeInstanceId: 'worker-instance' }) ) }) it('infers orchestration lineage from task creator when no dispatch context exists', async () => { const parentPath = '/tmp/worktree-parent' const childPath = '/tmp/workspaces/parent-child' const childId = `${TEST_REPO_ID}::${childPath}` const parentId = `${TEST_REPO_ID}::${parentPath}` const metaById: Record = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance', displayName: 'parent' }) } const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage) const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { const existing = metaById[worktreeId] ?? makeWorktreeMeta() metaById[worktreeId] = { ...existing, ...meta } return metaById[worktreeId] }, getWorktreeLineage: () => undefined, setWorktreeLineage } const runtime = new OrcaRuntimeService(runtimeStore as never) const parentHandle = runtime.preAllocateHandleForPty('pty-parent') runtime.setOrchestrationDb({ getDispatchContext: vi.fn(() => undefined), getTask: vi.fn(() => ({ id: 'task_creator123', created_by_terminal_handle: parentHandle })) } as never) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-parent', worktreeId: parentId, title: 'Parent', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-parent', worktreeId: parentId, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-parent', paneTitle: null } ] }) computeWorktreePathMock.mockReturnValue(childPath) ensurePathWithinWorkspaceMock.mockReturnValue(childPath) vi.mocked(listWorktrees) .mockResolvedValueOnce([ { path: parentPath, head: 'fed', branch: 'feature/parent', isBare: false, isMainWorktree: false } ]) .mockResolvedValueOnce([ { path: childPath, head: 'def', branch: 'parent-child', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'parent-child', comment: 'Created via orchestration task task_creator123' }) expect(result.lineage).toMatchObject({ worktreeId: childId, parentWorktreeId: parentId, origin: 'orchestration', capture: { source: 'orchestration-context', confidence: 'inferred' }, taskId: 'task_creator123' }) expect(setWorktreeLineage).toHaveBeenCalledWith( childId, expect.objectContaining({ parentWorktreeInstanceId: 'parent-instance' }) ) }) 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(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: 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', undefined ) 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, undefined, undefined ) }) it('passes setup payloads through when explicitly activating CLI-created worktrees', 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(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-hook-activate') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-hook-activate') 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-activate' } }) vi.mocked(listWorktrees).mockResolvedValueOnce([ { path: '/tmp/workspaces/runtime-hook-activate', head: 'def', branch: 'runtime-hook-activate', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-hook-activate', runHooks: true, activate: true }) expect(activateWorktree).toHaveBeenCalledWith( 'repo-1', expect.any(String), result.setup, undefined, undefined ) }) it('follows normal setup policy for CLI-created worktrees without activating them', async () => { const runtime = new OrcaRuntimeService(store) const activateWorktree = vi.fn() const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-created-worktree' }) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-primary' }) .mockResolvedValueOnce({ id: 'pty-setup' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: 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(shouldRunSetupForCreate).mockReturnValue(true) 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-skip' } }) vi.mocked(listWorktrees).mockResolvedValue([ { 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).toHaveBeenCalledWith( expect.objectContaining({ id: 'repo-1', path: '/tmp/repo' }), '/tmp/workspaces/runtime-hook-skip', 'pnpm worktree:setup', undefined ) expect(runHook).not.toHaveBeenCalled() expect(result).toEqual({ worktree: expect.objectContaining({ repoId: 'repo-1', path: '/tmp/workspaces/runtime-hook-skip', branch: 'runtime-hook-skip' }) }) expect(result.setup).toBeUndefined() expect(activateWorktree).not.toHaveBeenCalled() await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) expect(spawn).toHaveBeenNthCalledWith( 1, expect.objectContaining({ cwd: '/tmp/workspaces/runtime-hook-skip', command: undefined, worktreeId: result.worktree.id }) ) expect(spawn).toHaveBeenNthCalledWith( 2, expect.objectContaining({ cwd: '/tmp/workspaces/runtime-hook-skip', command: 'bash /tmp/repo/.git/orca/setup-runner.sh', // Why: createTerminal stamps ORCA_PANE_KEY/TAB_ID/WORKTREE_ID so hook-based agent status can attribute events to a stable pane. env: expect.objectContaining({ ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-hook-skip', ORCA_TAB_ID: expect.stringMatching(UUID_RE), ORCA_PANE_KEY: expect.any(String), ORCA_WORKTREE_ID: result.worktree.id }), worktreeId: result.worktree.id }) ) const setupSpawnEnv = (spawn.mock.calls[1]?.[0] as { env?: Record } | undefined)?.env ?? {} expectStablePaneKeyEnv(setupSpawnEnv) const setupLeafId = setupSpawnEnv.ORCA_PANE_KEY.slice(`${setupSpawnEnv.ORCA_TAB_ID}:`.length) expect(revealTerminalSession).toHaveBeenLastCalledWith(result.worktree.id, { ptyId: 'pty-setup', title: 'Setup', activate: false, tabId: setupSpawnEnv.ORCA_TAB_ID, leafId: setupLeafId }) }) it('sequences setup before startup for opted-in local headless worktree creates', async () => { const waitRepo = { ...store.getRepo('repo-1')!, hookSettings: { mode: 'auto' as const, setupRunPolicy: 'run-by-default' as const, setupAgentStartupPolicy: 'wait-for-setup' as const, scripts: { setup: '', archive: '' } } } const runtimeStore = { ...store, getRepos: () => [waitRepo], getRepo: (id: string) => (id === 'repo-1' ? waitRepo : undefined) } const runtime = new OrcaRuntimeService(runtimeStore as never) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-headless-startup' }) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-headless-startup' }) .mockResolvedValueOnce({ id: 'pty-headless-setup' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-headless-startup-setup') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-headless-startup-setup') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) vi.mocked(createSetupRunnerScript).mockReturnValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-headless-startup-setup' }, waitForAgentStartup: true }) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-headless-startup-setup', head: 'def', branch: 'runtime-headless-startup-setup', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-headless-startup-setup', setupDecision: 'run', startup: { command: 'claude' } }) expect(createSetupRunnerScript).toHaveBeenCalled() expect(runHook).not.toHaveBeenCalled() // Why: setup is provisioned fire-and-forget; the wait-for-setup guarantee comes from the shell nonce/marker, not JS spawn ordering. await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) const startupCommand = (spawn.mock.calls[0]![0] as { command: string }).command const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command const nonceMatch = startupCommand.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() expect(startupCommand).toContain('exec claude') expect(setupCommand).toContain('printf') expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) expect(result.setup).toBeUndefined() }) it('starts setup and startup side by side by default for local headless worktree creates', async () => { const runtime = new OrcaRuntimeService(store) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-headless-parallel' }) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-headless-parallel-startup' }) .mockResolvedValueOnce({ id: 'pty-headless-parallel-setup' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-headless-parallel') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-headless-parallel') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) vi.mocked(createSetupRunnerScript).mockReturnValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-headless-parallel' } }) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-headless-parallel', head: 'def', branch: 'runtime-headless-parallel', isBare: false, isMainWorktree: false } ]) await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-headless-parallel', setupDecision: 'run', startup: { command: 'claude' } }) // Why: setup now spawns fire-and-forget on a later tick; wait for both PTYs. await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) expect(spawn).toHaveBeenNthCalledWith(1, expect.objectContaining({ command: 'claude' })) expect(spawn).toHaveBeenNthCalledWith( 2, expect.objectContaining({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh' }) ) }) it('creates the first terminal for CLI-created worktrees without activating them', async () => { const runtime = new OrcaRuntimeService(store) const activateWorktree = vi.fn() const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-created-worktree' }) const spawn = vi.fn().mockResolvedValue({ id: 'pty-created-worktree' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-initial-terminal') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-initial-terminal') vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-initial-terminal', head: 'def', branch: 'runtime-initial-terminal', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-initial-terminal' }) expect(activateWorktree).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-initial-terminal', worktreeId: result.worktree.id, preAllocatedHandle: expect.stringMatching(/^term_/) }) ) const initialSpawnEnv = (spawn.mock.calls[0]?.[0] as { env?: Record } | undefined)?.env ?? {} expectStablePaneKeyEnv(initialSpawnEnv) const initialLeafId = initialSpawnEnv.ORCA_PANE_KEY.slice( `${initialSpawnEnv.ORCA_TAB_ID}:`.length ) expect(revealTerminalSession).toHaveBeenCalledWith(result.worktree.id, { ptyId: 'pty-created-worktree', title: null, activate: false, tabId: initialSpawnEnv.ORCA_TAB_ID, leafId: initialLeafId }) }) it('honors split setup placement for CLI-created worktrees without startup agents', async () => { const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), setupScriptLaunchMode: 'split-vertical' as const }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const activateWorktree = vi.fn() const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-cli-setup-split' }) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-cli-setup-main' }) .mockResolvedValueOnce({ id: 'pty-cli-setup-setup' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-cli-setup-split') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-cli-setup-split') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) vi.mocked(createSetupRunnerScript).mockReturnValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-cli-setup-split' } }) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-cli-setup-split', head: 'def', branch: 'runtime-cli-setup-split', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-cli-setup-split', setupDecision: 'run' }) expect(activateWorktree).not.toHaveBeenCalled() await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) const mainEnv = (spawn.mock.calls[0]![0] as { env?: Record }).env ?? {} const setupEnv = (spawn.mock.calls[1]![0] as { env?: Record }).env ?? {} expectStablePaneKeyEnv(mainEnv) expectStablePaneKeyEnv(setupEnv) expect(setupEnv.ORCA_TAB_ID).toBe(mainEnv.ORCA_TAB_ID) const mainLeafId = mainEnv.ORCA_PANE_KEY!.slice(`${mainEnv.ORCA_TAB_ID!}:`.length) expect(revealTerminalSession).toHaveBeenLastCalledWith( result.worktree.id, expect.objectContaining({ ptyId: 'pty-cli-setup-setup', tabId: mainEnv.ORCA_TAB_ID, activate: false, splitFromLeafId: mainLeafId, splitDirection: 'vertical' }) ) }) it('does not warn when setup is explicitly skipped for CLI-created worktrees', async () => { const metaById: Record = {} const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-cli-setup-skip' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-cli-setup-skip' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-cli-setup-skip') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-cli-setup-skip') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-cli-setup-skip', head: 'def', branch: 'runtime-cli-setup-skip', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-cli-setup-skip', setupDecision: 'skip' }) expect(result.warning).toBeUndefined() expect(createSetupRunnerScript).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledTimes(1) }) it('materializes default tabs for inactive local managed worktree creates', async () => { const metaById: Record = {} const runtimeStore = { ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-default-dev' }) .mockResolvedValueOnce({ id: 'pty-default-test' }) const revealTerminalSession = vi .fn() .mockResolvedValueOnce({ tabId: 'tab-default-dev' }) .mockResolvedValueOnce({ tabId: 'tab-default-test' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-default-tabs') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-default-tabs') vi.mocked(getDefaultTabsLaunch).mockReturnValue({ runCommands: true, tabs: [ { title: 'Dev', command: 'pnpm dev' }, { title: 'Test', command: 'pnpm test' } ] }) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-default-tabs', head: 'def', branch: 'runtime-default-tabs', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-default-tabs', setupDecision: 'run' }) expect(result.defaultTabs).toEqual({ runCommands: true, tabs: [ { title: 'Dev', command: 'pnpm dev' }, { title: 'Test', command: 'pnpm test' } ] }) await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) expect(spawn.mock.calls[0]![0]).toMatchObject({ command: 'pnpm dev' }) expect(spawn.mock.calls[1]![0]).toMatchObject({ command: 'pnpm test' }) expect(revealTerminalSession).toHaveBeenNthCalledWith( 1, result.worktree.id, expect.objectContaining({ title: 'Dev', activate: false }) ) expect(revealTerminalSession).toHaveBeenNthCalledWith( 2, result.worktree.id, expect.objectContaining({ title: 'Test', activate: false }) ) }) it('uses desktop task agent selection and bracketed-pastes startup drafts for local worktrees', async () => { detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValue(['claude']) const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), defaultTuiAgent: 'codex' as const, agentCmdOverrides: { codex: 'codex --profile work' } }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-startup-draft' }) const write = vi.fn().mockReturnValue(true) runtime.setPtyController({ spawn, write, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-startup-draft' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-startup-draft') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-startup-draft') vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-startup-draft', head: 'def', branch: 'runtime-startup-draft', isBare: false, isMainWorktree: false } ]) const draftUrl = 'https://github.com/stablyai/orca/issues/123' const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-startup-draft', startupDraft: draftUrl, activate: true }) expect(detectInstalledAgentsWithShellPathHydrationMock).not.toHaveBeenCalled() expect(detectRemoteAgentsMock).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-startup-draft', command: "codex --profile work '--dangerously-bypass-approvals-and-sandbox'", worktreeId: result.worktree.id }) ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'codex' }) runtime.onPtyData('pty-startup-draft', '\x1b[?2004h›', Date.now()) await vi.waitFor(() => { expect(write).toHaveBeenCalledWith('pty-startup-draft', `\x1b[200~${draftUrl}\x1b[201~`) }) }) it('rejects explicit startup commands for disabled selected agents', async () => { const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: ['codex' as const] }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-disabled-startup' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) await expect( runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'disabled-startup', startup: { command: 'codex' }, createdWithAgent: 'codex' }) ).rejects.toThrow('Selected agent is disabled. Choose an enabled agent before creating.') expect(spawn).not.toHaveBeenCalled() expect(addWorktree).not.toHaveBeenCalled() }) it('launches explicit startup agents with prompts for CLI-created worktrees', async () => { const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-cli-agent-startup' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-cli-agent-startup' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-cli-agent-startup') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-cli-agent-startup') vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-cli-agent-startup', head: 'def', branch: 'runtime-cli-agent-startup', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'runtime-cli-agent-startup', startupAgent: 'codex', startupPrompt: 'hi' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-cli-agent-startup', command: "codex '--dangerously-bypass-approvals-and-sandbox' 'hi'", worktreeId: result.worktree.id }) ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'codex' }) }) it('sends follow-up prompts for CLI-created stdin-after-start startup agents', async () => { const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-cli-aider-startup' }) const write = vi.fn().mockReturnValue(true) runtime.setPtyController({ spawn, write, kill: () => true, getForegroundProcess: async () => 'aider' }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-cli-aider-startup' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-cli-aider-startup') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-cli-aider-startup') vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-cli-aider-startup', head: 'def', branch: 'runtime-cli-aider-startup', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'runtime-cli-aider-startup', startupAgent: 'aider', startupPrompt: 'fix it' }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-cli-aider-startup', command: "aider '--yes-always'", worktreeId: result.worktree.id }) ) await vi.waitFor(() => { expect(write).toHaveBeenCalledWith('pty-cli-aider-startup', 'fix it\r') }) }) it('does not send stdin-after-start prompts into a shell when the agent never starts', async () => { vi.useFakeTimers() try { const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const write = vi.fn().mockReturnValue(true) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-cli-aider-shell' }), write, kill: () => true, getForegroundProcess: async () => 'zsh', hasChildProcesses: vi.fn().mockResolvedValue(false) }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-cli-aider-shell' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-cli-aider-shell') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-cli-aider-shell') vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-cli-aider-shell', head: 'def', branch: 'runtime-cli-aider-shell', isBare: false, isMainWorktree: false } ]) await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'runtime-cli-aider-shell', startupAgent: 'aider', startupPrompt: 'fix it' }) await vi.advanceTimersByTimeAsync(6000) expect(write).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('records the resolved fallback agent when the requested startup draft agent is disabled', async () => { detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValue(['claude']) const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), defaultTuiAgent: 'codex' as const, disabledTuiAgents: ['codex' as const], agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-fallback-draft' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-fallback-draft' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-fallback-draft') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-fallback-draft') vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-fallback-draft', head: 'def', branch: 'runtime-fallback-draft', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'runtime-fallback-draft', startupDraft: 'https://github.com/stablyai/orca/issues/456', createdWithAgent: 'codex', activate: true }) expect(detectInstalledAgentsWithShellPathHydrationMock).toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-fallback-draft', command: expect.stringContaining('claude'), worktreeId: result.worktree.id }) ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'claude' }) }) it('honors split setup placement for opted-in local startup-draft worktrees', async () => { const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), defaultTuiAgent: 'codex' as const, setupScriptLaunchMode: 'split-vertical' as const }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-startup-split-main' }) .mockResolvedValueOnce({ id: 'pty-startup-split-setup' }) const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-startup-split' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession, splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-startup-setup-split') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-startup-setup-split') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) vi.mocked(createSetupRunnerScript).mockReturnValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-startup-setup-split' }, waitForAgentStartup: true }) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-startup-setup-split', head: 'def', branch: 'runtime-startup-setup-split', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-startup-setup-split', startupDraft: 'https://github.com/stablyai/orca/issues/123', setupDecision: 'run', activate: true }) await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) expect(spawn).toHaveBeenNthCalledWith( 1, expect.objectContaining({ cwd: '/tmp/workspaces/runtime-startup-setup-split', command: expect.stringContaining('codex'), worktreeId: result.worktree.id }) ) expect(spawn).toHaveBeenNthCalledWith( 2, expect.objectContaining({ cwd: '/tmp/workspaces/runtime-startup-setup-split', command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh'), env: expect.objectContaining({ ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-startup-setup-split', ORCA_WORKTREE_ID: result.worktree.id }), worktreeId: result.worktree.id }) ) const startupCommand = (spawn.mock.calls[0]![0] as { command: string }).command const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command const nonceMatch = startupCommand.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() const markerPath = `/tmp/repo/.git/orca/setup-runner.sh.${nonceMatch![1]}.done` expect(startupCommand).toContain('--dangerously-bypass-approvals-and-sandbox') expect(setupCommand).toContain('printf') expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) expect(startupCommand).toContain(markerPath) expect(setupCommand).toContain(markerPath) const mainEnv = (spawn.mock.calls[0]![0] as { env?: Record }).env ?? {} const setupEnv = (spawn.mock.calls[1]![0] as { env?: Record }).env ?? {} expect(result.setup).toBeUndefined() expect(mainEnv.ORCA_TAB_ID).toBeDefined() expect(mainEnv.ORCA_PANE_KEY).toBeDefined() expect(setupEnv.ORCA_TAB_ID).toBe(mainEnv.ORCA_TAB_ID) const mainLeafId = mainEnv.ORCA_PANE_KEY!.slice(`${mainEnv.ORCA_TAB_ID!}:`.length) expect(revealTerminalSession).toHaveBeenLastCalledWith( result.worktree.id, expect.objectContaining({ ptyId: 'pty-startup-split-setup', tabId: mainEnv.ORCA_TAB_ID, activate: false, splitFromLeafId: mainLeafId, splitDirection: 'vertical' }) ) }) it('passes the wrapped setup command to activation when startup spawned but setup did not', async () => { const runtime = new OrcaRuntimeService(store) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'pty-startup-main' }) .mockRejectedValueOnce(new Error('setup spawn failed')) const activateWorktree = vi.fn() runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-startup-main' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-startup-setup-retry') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-startup-setup-retry') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) vi.mocked(createSetupRunnerScript).mockReturnValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-startup-setup-retry' }, waitForAgentStartup: true }) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-startup-setup-retry', head: 'def', branch: 'runtime-startup-setup-retry', isBare: false, isMainWorktree: false } ]) await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-startup-setup-retry', setupDecision: 'run', activate: true, startup: { command: 'claude' } }) expect(spawn).toHaveBeenCalledTimes(2) expect(activateWorktree).toHaveBeenCalledWith( 'repo-1', expect.any(String), expect.objectContaining({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') }), undefined, undefined ) const activationSetup = activateWorktree.mock.calls[0]?.[2] as { command?: string } | undefined expect(activationSetup?.command).toContain('printf') }) it('lets explicit startup draft agents override the desktop default', async () => { detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValue([]) const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), defaultTuiAgent: 'claude' as const, agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-explicit-draft' }) const write = vi.fn().mockReturnValue(true) runtime.setPtyController({ spawn, write, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-explicit-draft' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-explicit-draft') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-explicit-draft') vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-explicit-draft', head: 'def', branch: 'runtime-explicit-draft', isBare: false, isMainWorktree: false } ]) const draftUrl = 'https://github.com/stablyai/orca/issues/789' const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-explicit-draft', startupDraft: draftUrl, createdWithAgent: 'codex', activate: true }) expect(detectInstalledAgentsWithShellPathHydrationMock).not.toHaveBeenCalled() expect(detectRemoteAgentsMock).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-explicit-draft', command: "codex '--dangerously-bypass-approvals-and-sandbox'", worktreeId: result.worktree.id }) ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'codex' }) runtime.onPtyData('pty-explicit-draft', '\x1b[?2004h›', Date.now()) await vi.waitFor(() => { expect(write).toHaveBeenCalledWith('pty-explicit-draft', `\x1b[200~${draftUrl}\x1b[201~`) }) }) it('does not auto-launch an agent for startup drafts when the default is blank', async () => { detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValue(['claude', 'codex']) const metaById: Record = {} const runtimeStore = { ...store, getSettings: () => ({ ...store.getSettings(), defaultTuiAgent: 'blank' as const, agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const runtime = new OrcaRuntimeService(runtimeStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-blank-draft' }) const activateWorktree = vi.fn() runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree, createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-blank-draft' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-blank-draft') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-blank-draft') vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-blank-draft', head: 'def', branch: 'runtime-blank-draft', isBare: false, isMainWorktree: false } ]) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-blank-draft', startupDraft: 'https://github.com/stablyai/orca/issues/123', activate: true }) expect(detectInstalledAgentsWithShellPathHydrationMock).not.toHaveBeenCalled() expect(detectRemoteAgentsMock).not.toHaveBeenCalled() expect(spawn).not.toHaveBeenCalled() expect(metaById[result.worktree.id]?.createdWithAgent).toBeUndefined() expect(activateWorktree).toHaveBeenCalledWith( 'repo-1', result.worktree.id, undefined, undefined, undefined ) }) it('detects agents on the SSH host before launching remote startup drafts', async () => { detectRemoteAgentsMock.mockResolvedValue(['claude']) const created = { path: '/remote/mobile-startup-draft', head: 'def', branch: 'refs/heads/mobile-startup-draft', isBare: false, isMainWorktree: false } const metaById: Record = {} const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getRepo: () => ({ id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' }), getSettings: () => ({ ...store.getSettings(), defaultTuiAgent: null, agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } registerSshGitProvider('ssh-1', provider as never) getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() }) const runtime = new OrcaRuntimeService(remoteStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-remote-startup-draft' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const draftUrl = 'https://github.com/stablyai/orca/pull/456' const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'mobile-startup-draft', startupDraft: draftUrl }) expect(detectRemoteAgentsMock).toHaveBeenCalledWith({ connectionId: 'ssh-1' }) expect(detectInstalledAgentsWithShellPathHydrationMock).not.toHaveBeenCalled() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/remote/mobile-startup-draft', command: `claude '--dangerously-skip-permissions' --prefill '${draftUrl}'`, connectionId: 'ssh-1', worktreeId: result.worktree.id }) ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'claude' }) }) it('pre-marks remote Codex workspaces trusted before pasting startup drafts', async () => { detectRemoteAgentsMock.mockResolvedValue(['codex']) muxRequestMock.mockResolvedValue({ resolvedPath: '/home/dev' }) const created = { path: '/remote/mobile-codex-draft', head: 'def', branch: 'refs/heads/mobile-codex-draft', isBare: false, isMainWorktree: false } const metaById: Record = {} const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getRepo: () => ({ id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' }), getSettings: () => ({ ...store.getSettings(), defaultTuiAgent: 'codex' as const, agentCmdOverrides: {} }), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const gitProvider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } const fsProvider = { realpath: vi.fn().mockResolvedValue('/remote/mobile-codex-draft'), readFile: vi.fn().mockRejectedValue(new Error('missing config')), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } registerSshGitProvider('ssh-1', gitProvider as never) registerSshFilesystemProvider('ssh-1', fsProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-remote-codex-draft' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) try { const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'mobile-codex-draft', startupDraft: 'https://github.com/stablyai/orca/issues/789' }) expect(detectRemoteAgentsMock).not.toHaveBeenCalled() expect(muxRequestMock).toHaveBeenCalledWith('session.resolveHome', { path: '~' }) expect(fsProvider.createDir).toHaveBeenCalledWith('/home/dev/.codex') expect(fsProvider.writeFile).toHaveBeenCalledWith( '/home/dev/.codex/config.toml', expect.stringContaining('[projects."/remote/mobile-codex-draft"]') ) expect(fsProvider.writeFile).toHaveBeenCalledWith( '/home/dev/.codex/config.toml', expect.stringContaining('trust_level = "trusted"') ) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/remote/mobile-codex-draft', command: "codex '--dangerously-bypass-approvals-and-sandbox'", connectionId: 'ssh-1', worktreeId: result.worktree.id }) ) expect(fsProvider.writeFile.mock.invocationCallOrder[0]).toBeLessThan( spawn.mock.invocationCallOrder[0]! ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'codex' }) } finally { unregisterSshFilesystemProvider('ssh-1') unregisterSshGitProvider('ssh-1') } }) it('pre-marks remote Codex workspaces trusted before explicit startup commands', async () => { muxRequestMock.mockResolvedValue({ resolvedPath: '/home/dev' }) const created = { path: '/remote/mobile-codex-command', head: 'def', branch: 'refs/heads/mobile-codex-command', isBare: false, isMainWorktree: false } const metaById: Record = {} const remoteStore = { ...store, getRepos: () => [ { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' } ], getRepo: () => ({ id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1' }), getSettings: () => store.getSettings(), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] } } const gitProvider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'config') { return { stdout: 'Remote User\n', stderr: '' } } if (args[0] === 'branch') { return { stdout: '', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([created]) } const fsProvider = { realpath: vi.fn().mockResolvedValue('/remote/mobile-codex-command'), readFile: vi.fn().mockRejectedValue(new Error('missing config')), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } registerSshGitProvider('ssh-1', gitProvider as never) registerSshFilesystemProvider('ssh-1', fsProvider as never) const runtime = new OrcaRuntimeService(remoteStore as never) const spawn = vi.fn().mockResolvedValue({ id: 'pty-remote-codex-command' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) try { const result = await runtime.createManagedWorktree({ repoSelector: TEST_REPO_ID, name: 'mobile-codex-command', startup: { command: 'codex' }, createdWithAgent: 'codex' }) expect(detectRemoteAgentsMock).not.toHaveBeenCalled() expect(muxRequestMock).toHaveBeenCalledWith('session.resolveHome', { path: '~' }) expect(fsProvider.writeFile).toHaveBeenCalledWith( '/home/dev/.codex/config.toml', expect.stringContaining('[projects."/remote/mobile-codex-command"]') ) expect(fsProvider.writeFile).toHaveBeenCalledWith( '/home/dev/.codex/config.toml', expect.stringContaining('trust_level = "trusted"') ) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/remote/mobile-codex-command', command: 'codex', connectionId: 'ssh-1', worktreeId: result.worktree.id }) ) expect(fsProvider.writeFile.mock.invocationCallOrder[0]).toBeLessThan( spawn.mock.invocationCallOrder[0]! ) expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'codex' }) } finally { unregisterSshFilesystemProvider('ssh-1') unregisterSshGitProvider('ssh-1') } }) it('passes SSH connection ids through GitLab task operations', async () => { listGitLabMergeRequestsMock.mockResolvedValue({ items: [] }) listGitLabWorkItemsMock.mockResolvedValue({ items: [] }) listGitLabIssuesMock.mockResolvedValue({ items: [ { number: 7, title: 'Issue title', state: 'opened', url: 'https://gitlab.example/issues/7', labels: ['bug'], updatedAt: '2026-05-22T00:00:00Z', author: 'alex' } ] }) listGitLabTodosMock.mockResolvedValue([]) listGitLabLabelsMock.mockResolvedValue(['bug', 'frontend']) getGitLabWorkItemByProjectRefMock.mockResolvedValue({ id: 'gitlab-issue-7', type: 'issue', number: 7 }) createGitLabIssueMock.mockResolvedValue({ ok: true, number: 1, url: 'https://gitlab.example/issues/1' }) updateGitLabIssueMock.mockResolvedValue({ ok: true }) addGitLabIssueCommentMock.mockResolvedValue({ ok: true }) addGitLabMRCommentMock.mockResolvedValue({ ok: true }) addGitLabMRInlineCommentMock.mockResolvedValue({ ok: true }) resolveGitLabMRDiscussionMock.mockResolvedValue({ ok: true }) getGitLabJobTraceMock.mockResolvedValue({ ok: true, trace: 'log' }) retryGitLabJobMock.mockResolvedValue({ ok: true }) mergeGitLabMRMock.mockResolvedValue({ ok: true }) closeGitLabMRMock.mockResolvedValue({ ok: true }) reopenGitLabMRMock.mockResolvedValue({ ok: true }) getGitLabWorkItemDetailsMock.mockResolvedValue({ body: 'Details' }) updateGitLabMRReviewersMock.mockResolvedValue({ ok: true, reviewers: [] }) const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1', issueSourcePreference: 'origin' as const } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined) } const runtime = new OrcaRuntimeService(runtimeStore as never) await runtime.listGitLabRepoMRs(TEST_REPO_ID, 'closed', 2, 25, 'ambiguous selector') await runtime.listGitLabRepoWorkItems(TEST_REPO_ID, 'closed', 2, 25, 'ambiguous selector') const issues = await runtime.listGitLabRepoIssues(TEST_REPO_ID, 'opened', '@me', 50) await runtime.listGitLabRepoTodos(TEST_REPO_ID) await runtime.listGitLabRepoLabels(TEST_REPO_ID) await runtime.createGitLabRepoIssue(TEST_REPO_ID, 'New issue', 'Body') await runtime.updateGitLabRepoIssue(TEST_REPO_ID, 7, { state: 'closed' }) await runtime.addGitLabRepoIssueComment(TEST_REPO_ID, 7, 'Looks good') await runtime.addGitLabRepoMRComment(TEST_REPO_ID, 8, 'Ship it') const inlineCommentInput = { body: 'please fix', path: 'src/app.ts', line: 12, baseSha: 'base', startSha: 'start', headSha: 'head' } await runtime.addGitLabRepoMRInlineComment(TEST_REPO_ID, 8, inlineCommentInput) await runtime.resolveGitLabRepoMRDiscussion(TEST_REPO_ID, 8, 'discussion-1', true) await runtime.getGitLabRepoJobTrace(TEST_REPO_ID, 99) await runtime.retryGitLabRepoJob(TEST_REPO_ID, 99) await runtime.mergeGitLabRepoMR(TEST_REPO_ID, 8, 'squash') await runtime.updateGitLabRepoMRState(TEST_REPO_ID, 8, 'closed') await runtime.updateGitLabRepoMRState(TEST_REPO_ID, 8, 'opened') await runtime.getGitLabRepoWorkItemDetails(TEST_REPO_ID, 8, 'mr') await runtime.updateGitLabRepoMRReviewers(TEST_REPO_ID, 8, [1, 2]) await runtime.getGitLabRepoWorkItemByPath( TEST_REPO_ID, { host: 'gitlab.example.com', path: 'group/project' }, 7, 'issue' ) expect(listGitLabMergeRequestsMock).toHaveBeenCalledWith( '/remote/repo', 'closed', 2, 25, 'origin', 'ambiguous selector', 'ssh-1' ) expect(listGitLabWorkItemsMock).toHaveBeenCalledWith( '/remote/repo', 'closed', 2, 25, 'origin', 'ambiguous selector', 'ssh-1' ) expect(listGitLabIssuesMock).toHaveBeenCalledWith( '/remote/repo', 50, 'origin', 'opened', '@me', 'ssh-1' ) expect(issues.items).toEqual([ { id: `gitlab-issue-${TEST_REPO_ID}-7`, type: 'issue', number: 7, title: 'Issue title', state: 'opened', url: 'https://gitlab.example/issues/7', labels: ['bug'], updatedAt: '2026-05-22T00:00:00Z', author: 'alex', repoId: TEST_REPO_ID } ]) expect(listGitLabTodosMock).toHaveBeenCalledWith('/remote/repo', 'ssh-1') expect(listGitLabLabelsMock).toHaveBeenCalledWith('/remote/repo', 'origin', 'ssh-1') expect(createGitLabIssueMock).toHaveBeenCalledWith( '/remote/repo', 'New issue', 'Body', 'origin', 'ssh-1' ) expect(updateGitLabIssueMock).toHaveBeenCalledWith( '/remote/repo', 7, { state: 'closed' }, 'origin', 'ssh-1', undefined ) expect(addGitLabIssueCommentMock).toHaveBeenCalledWith( '/remote/repo', 7, 'Looks good', 'origin', 'ssh-1', undefined ) expect(addGitLabMRCommentMock).toHaveBeenCalledWith( '/remote/repo', 8, 'Ship it', 'origin', 'ssh-1', undefined ) expect(addGitLabMRInlineCommentMock).toHaveBeenCalledWith( '/remote/repo', 8, inlineCommentInput, 'origin', 'ssh-1', undefined ) expect(resolveGitLabMRDiscussionMock).toHaveBeenCalledWith( '/remote/repo', 8, 'discussion-1', true, 'origin', 'ssh-1', undefined ) expect(getGitLabJobTraceMock).toHaveBeenCalledWith( '/remote/repo', 99, 'origin', 'ssh-1', undefined ) expect(retryGitLabJobMock).toHaveBeenCalledWith( '/remote/repo', 99, 'origin', 'ssh-1', undefined ) expect(mergeGitLabMRMock).toHaveBeenCalledWith( '/remote/repo', 8, 'squash', 'origin', 'ssh-1', undefined ) expect(closeGitLabMRMock).toHaveBeenCalledWith('/remote/repo', 8, 'origin', 'ssh-1', undefined) expect(reopenGitLabMRMock).toHaveBeenCalledWith('/remote/repo', 8, 'origin', 'ssh-1', undefined) expect(getGitLabWorkItemDetailsMock).toHaveBeenCalledWith( '/remote/repo', 8, 'mr', 'origin', 'ssh-1', undefined ) expect(updateGitLabMRReviewersMock).toHaveBeenCalledWith( '/remote/repo', 8, [1, 2], 'origin', 'ssh-1', undefined ) expect(getGitLabWorkItemByProjectRefMock).toHaveBeenCalledWith( '/remote/repo', { host: 'gitlab.example.com', path: 'group/project' }, 7, 'issue', 'ssh-1' ) }) it('routes runtime GitLab issue, MR, work-item, and todo actions through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const localGitOptions = { wslDistro: 'Ubuntu' } listGitLabMergeRequestsMock.mockResolvedValue({ items: [] }) listGitLabWorkItemsMock.mockResolvedValue({ items: [] }) listGitLabIssuesMock.mockResolvedValue({ items: [] }) listGitLabTodosMock.mockResolvedValue([]) listGitLabLabelsMock.mockResolvedValue([]) createGitLabIssueMock.mockResolvedValue({ ok: true, number: 7, url: 'https://gitlab.example/issues/7' }) updateGitLabIssueMock.mockResolvedValue({ ok: true }) addGitLabIssueCommentMock.mockResolvedValue({ ok: true }) await runtime.listGitLabRepoMRs(TEST_REPO_ID, 'opened', 1, 20) await runtime.listGitLabRepoWorkItems(TEST_REPO_ID, 'opened', 1, 20) await runtime.listGitLabRepoIssues(TEST_REPO_ID, 'opened', undefined, 20) await runtime.listGitLabRepoTodos(TEST_REPO_ID) await runtime.listGitLabRepoLabels(TEST_REPO_ID) await runtime.createGitLabRepoIssue(TEST_REPO_ID, 'Title', 'Body') await runtime.updateGitLabRepoIssue(TEST_REPO_ID, 7, { body: 'Updated' }) await runtime.addGitLabRepoIssueComment(TEST_REPO_ID, 7, 'Comment') expect(listGitLabMergeRequestsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'opened', 1, 20, undefined, undefined, null, localGitOptions ) expect(listGitLabWorkItemsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'opened', 1, 20, undefined, undefined, null, localGitOptions ) expect(listGitLabIssuesMock).toHaveBeenCalledWith( TEST_REPO_PATH, 20, undefined, 'opened', undefined, null, localGitOptions ) expect(listGitLabTodosMock).toHaveBeenCalledWith(TEST_REPO_PATH, null, localGitOptions) expect(listGitLabLabelsMock).toHaveBeenCalledWith( TEST_REPO_PATH, undefined, null, localGitOptions ) expect(createGitLabIssueMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'Title', 'Body', undefined, null, localGitOptions ) expect(updateGitLabIssueMock).toHaveBeenCalledWith( TEST_REPO_PATH, 7, { body: 'Updated' }, undefined, null, undefined, localGitOptions ) expect(addGitLabIssueCommentMock).toHaveBeenCalledWith( TEST_REPO_PATH, 7, 'Comment', undefined, null, undefined, localGitOptions ) }) it('routes runtime GitLab MR details, review-management, job, and pasted URL actions through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const localGitOptions = { wslDistro: 'Ubuntu' } const inlineInput = { body: 'Inline', path: 'src/app.ts', line: 12, baseSha: 'base', startSha: 'start', headSha: 'head' } getGitLabWorkItemDetailsMock.mockResolvedValue({ body: 'Details' }) updateGitLabMRMock.mockResolvedValue({ ok: true }) updateGitLabMRReviewersMock.mockResolvedValue({ ok: true, reviewers: [] }) addGitLabMRCommentMock.mockResolvedValue({ ok: true }) addGitLabMRInlineCommentMock.mockResolvedValue({ ok: true }) resolveGitLabMRDiscussionMock.mockResolvedValue({ ok: true }) getGitLabJobTraceMock.mockResolvedValue({ ok: true, trace: 'trace' }) retryGitLabJobMock.mockResolvedValue({ ok: true }) mergeGitLabMRMock.mockResolvedValue({ ok: true }) closeGitLabMRMock.mockResolvedValue({ ok: true }) reopenGitLabMRMock.mockResolvedValue({ ok: true }) getGitLabWorkItemByProjectRefMock.mockResolvedValue({ type: 'mr', number: 8 }) await runtime.getGitLabRepoWorkItemDetails(TEST_REPO_ID, 8, 'mr') await runtime.updateGitLabRepoMR(TEST_REPO_ID, 8, { title: 'Renamed' }) await runtime.updateGitLabRepoMRReviewers(TEST_REPO_ID, 8, [1]) await runtime.addGitLabRepoMRComment(TEST_REPO_ID, 8, 'Comment') await runtime.addGitLabRepoMRInlineComment(TEST_REPO_ID, 8, inlineInput) await runtime.resolveGitLabRepoMRDiscussion(TEST_REPO_ID, 8, 'discussion-1', true) await runtime.getGitLabRepoJobTrace(TEST_REPO_ID, 99) await runtime.retryGitLabRepoJob(TEST_REPO_ID, 99) await runtime.mergeGitLabRepoMR(TEST_REPO_ID, 8, 'squash') await runtime.updateGitLabRepoMRState(TEST_REPO_ID, 8, 'closed') await runtime.updateGitLabRepoMRState(TEST_REPO_ID, 8, 'opened') await runtime.getGitLabRepoWorkItemByPath( TEST_REPO_ID, { host: 'gitlab.com', path: 'g/p' }, 8, 'mr' ) expect(getGitLabWorkItemDetailsMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, 'mr', undefined, null, undefined, localGitOptions ) expect(updateGitLabMRMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, { title: 'Renamed' }, undefined, null, undefined, localGitOptions ) expect(updateGitLabMRReviewersMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, [1], undefined, null, undefined, localGitOptions ) expect(addGitLabMRCommentMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, 'Comment', undefined, null, undefined, localGitOptions ) expect(addGitLabMRInlineCommentMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, inlineInput, undefined, null, undefined, localGitOptions ) expect(resolveGitLabMRDiscussionMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, 'discussion-1', true, undefined, null, undefined, localGitOptions ) expect(getGitLabJobTraceMock).toHaveBeenCalledWith( TEST_REPO_PATH, 99, undefined, null, undefined, localGitOptions ) expect(retryGitLabJobMock).toHaveBeenCalledWith( TEST_REPO_PATH, 99, undefined, null, undefined, localGitOptions ) expect(mergeGitLabMRMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, 'squash', undefined, null, undefined, localGitOptions ) expect(closeGitLabMRMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, undefined, null, undefined, localGitOptions ) expect(reopenGitLabMRMock).toHaveBeenCalledWith( TEST_REPO_PATH, 8, undefined, null, undefined, localGitOptions ) expect(getGitLabWorkItemByProjectRefMock).toHaveBeenCalledWith( TEST_REPO_PATH, { host: 'gitlab.com', path: 'g/p' }, 8, 'mr', null, localGitOptions ) }) it('normalizes runtime GitLab issue list arguments like the desktop IPC path', async () => { const runtime = new OrcaRuntimeService(store as never) await runtime.listGitLabRepoIssues(TEST_REPO_ID, 'closed', 'someone-else' as never, 250.8) await runtime.listGitLabRepoIssues(TEST_REPO_ID, 'all', '@me', 0.7) await runtime.listGitLabRepoIssues(TEST_REPO_ID, 'unexpected' as never, '@me', Number.NaN) expect(listGitLabIssuesMock).toHaveBeenNthCalledWith( 1, TEST_REPO_PATH, 100, undefined, 'closed', undefined, null ) expect(listGitLabIssuesMock).toHaveBeenNthCalledWith( 2, TEST_REPO_PATH, 1, undefined, 'all', '@me', null ) expect(listGitLabIssuesMock).toHaveBeenNthCalledWith( 3, TEST_REPO_PATH, 20, undefined, 'opened', '@me', null ) }) it('records GitLab pasted-project recents only after successful runtime lookup', async () => { let settings = { ...store.getSettings(), gitlabProjects: { pinned: [{ host: 'gitlab.example.com', path: 'group/pinned' }], recent: [] } } const updateSettings = vi.fn((updates: Record) => { settings = { ...settings, ...updates } as typeof settings }) const runtimeStore = { ...store, getSettings: () => settings, updateSettings } const runtime = new OrcaRuntimeService(runtimeStore as never) getGitLabWorkItemByProjectRefMock.mockResolvedValueOnce({ id: 'gitlab-issue-7', type: 'issue', number: 7 }) await runtime.getGitLabRepoWorkItemByPath( TEST_REPO_ID, { host: 'gitlab.example.com', path: 'group/project' }, 7, 'issue' ) expect(updateSettings).toHaveBeenCalledWith({ gitlabProjects: { pinned: [{ host: 'gitlab.example.com', path: 'group/pinned' }], recent: [ expect.objectContaining({ host: 'gitlab.example.com', path: 'group/project', lastOpenedAt: expect.any(String) }) ] } }) updateSettings.mockClear() getGitLabWorkItemByProjectRefMock.mockResolvedValueOnce(null) await runtime.getGitLabRepoWorkItemByPath( TEST_REPO_ID, { host: 'gitlab.example.com', path: 'group/missing' }, 404, 'issue' ) expect(updateSettings).not.toHaveBeenCalled() }) it('routes runtime GitHub PR base git calls through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (isOriginMainBaseRefProbe(args)) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'config') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } if ( args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'origin/feature/add-feature' ) { return { stdout: 'pr-head-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }) gitSpy.mockClear() try { const result = await runtime.resolveManagedPrBase({ repoSelector: 'id:repo-1', prNumber: 42, headRefName: 'feature/add-feature', isCrossRepository: false }) expect(result).toMatchObject({ baseBranch: 'pr-head-sha', headSha: 'pr-head-sha', branchNameOverride: 'feature/add-feature' }) expect(gitSpy).toHaveBeenCalledWith(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { cwd: TEST_REPO_PATH, timeout: 15_000, wslDistro: 'Ubuntu' }) expect(gitSpy).toHaveBeenCalledWith( [ 'fetch', 'origin', '+refs/heads/feature/add-feature:refs/remotes/origin/feature/add-feature' ], { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' } ) expect(gitSpy).toHaveBeenCalledWith(['rev-parse', '--verify', 'origin/feature/add-feature'], { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' }) } finally { gitSpy.mockRestore() } }) it('resolves local GitLab fork MR bases from the target project MR head ref', async () => { const localRepo = { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1, issueSourcePreference: 'origin' as const } const runtimeStore = { ...store, getRepos: () => [localRepo], getRepo: (id: string) => (id === localRepo.id ? localRepo : undefined) } getGitLabProjectRefForRemoteMock.mockResolvedValue({ host: 'gitlab.example', path: 'group/repo' }) const runtime = new OrcaRuntimeService(runtimeStore as never) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } if (args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'FETCH_HEAD') { return { stdout: 'fork-mr-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }) gitSpy.mockClear() try { const result = await runtime.resolveManagedMrBase({ repoSelector: 'id:repo-1', mrIid: 42, sourceBranch: 'contrib/fix', targetBranch: 'main', isCrossRepository: true }) expect(result).toEqual({ baseBranch: 'fork-mr-sha', compareBaseRef: 'refs/remotes/origin/main' }) expect(gitSpy).toHaveBeenCalledWith(['fetch', 'origin', 'refs/merge-requests/42/head'], { cwd: TEST_REPO_PATH }) expect(gitSpy).toHaveBeenCalledWith( ['fetch', 'origin', '+refs/heads/main:refs/remotes/origin/main'], { cwd: TEST_REPO_PATH } ) expect(gitSpy).toHaveBeenCalledWith(['rev-parse', '--verify', 'FETCH_HEAD'], { cwd: TEST_REPO_PATH }) } finally { gitSpy.mockRestore() } }) it('routes runtime GitLab fork MR base git calls through the selected WSL project runtime', async () => { setPlatform('win32') const localRepo = { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1, issueSourcePreference: 'origin' as const } const runtimeStore = { ...store, getRepos: () => [localRepo], getRepo: (id: string) => (id === localRepo.id ? localRepo : undefined), getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } if (args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'FETCH_HEAD') { return { stdout: 'fork-mr-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }) gitSpy.mockClear() getGlabKnownHostsMock.mockResolvedValue(['gitlab.com', 'git.internal']) try { const result = await runtime.resolveManagedMrBase({ repoSelector: 'id:repo-1', mrIid: 42, sourceBranch: 'contrib/fix', isCrossRepository: true }) expect(result).toEqual({ baseBranch: 'fork-mr-sha' }) expect(getGitLabProjectRefForRemoteMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'origin', ['gitlab.com', 'git.internal'], null, { wslDistro: 'Ubuntu' } ) expect(gitSpy).toHaveBeenCalledWith(['fetch', 'origin', 'refs/merge-requests/42/head'], { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' }) expect(gitSpy).toHaveBeenCalledWith(['rev-parse', '--verify', 'FETCH_HEAD'], { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' }) } finally { gitSpy.mockRestore() } }) it('resolves SSH GitLab fork MR bases from the target project MR head ref', async () => { const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1', issueSourcePreference: 'origin' as const } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined) } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'FETCH_HEAD') { return { stdout: 'remote-fork-mr-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), fetchGitLabMergeRequestHead: vi.fn().mockResolvedValue(undefined), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined) } registerSshGitProvider('ssh-1', provider as never) getGlabKnownHostsMock.mockResolvedValue(['gitlab.com', 'git.internal']) const runtime = new OrcaRuntimeService(runtimeStore as never) const result = await runtime.resolveManagedMrBase({ repoSelector: 'id:repo-1', mrIid: 77, sourceBranch: 'contrib/remote-fix', targetBranch: 'main', isCrossRepository: true }) expect(result).toEqual({ baseBranch: 'remote-fork-mr-sha', compareBaseRef: 'refs/remotes/origin/main' }) expect(provider.fetchGitLabMergeRequestHead).toHaveBeenCalledWith('/remote/repo', 'origin', 77) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'main', 'refs/remotes/origin/main' ) expect(provider.exec).toHaveBeenCalledWith( ['rev-parse', '--verify', 'FETCH_HEAD'], '/remote/repo' ) expect(getGitLabProjectRefForRemoteMock).toHaveBeenCalledWith( '/remote/repo', 'origin', ['gitlab.com', 'git.internal'], 'ssh-1' ) }) it('resolves SSH GitLab same-repo MR bases through remote-tracking fetches', async () => { const remoteRepo = { id: TEST_REPO_ID, path: '/remote/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-1', issueSourcePreference: 'origin' as const } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined) } const provider = { exec: vi.fn(async (args: string[]) => { if (args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'origin/feature/fix') { return { stdout: 'same-repo-mr-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }), fetchGitLabMergeRequestHead: vi.fn().mockResolvedValue(undefined), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined) } registerSshGitProvider('ssh-1', provider as never) getGlabKnownHostsMock.mockResolvedValue(['gitlab.com', 'git.internal']) getGitLabProjectRefForRemoteMock.mockResolvedValue({ host: 'gitlab.example', path: 'group/repo' }) const runtime = new OrcaRuntimeService(runtimeStore as never) const result = await runtime.resolveManagedMrBase({ repoSelector: 'id:repo-1', mrIid: 78, sourceBranch: 'feature/fix', targetBranch: 'main' }) expect(result).toEqual({ baseBranch: 'origin/feature/fix', compareBaseRef: 'refs/remotes/origin/main', pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'feature/fix', 'refs/remotes/origin/feature/fix' ) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'main', 'refs/remotes/origin/main' ) expect(provider.fetchGitLabMergeRequestHead).not.toHaveBeenCalled() expect(provider.exec).toHaveBeenCalledWith( ['rev-parse', '--verify', 'origin/feature/fix'], '/remote/repo' ) }) it('keeps the MR source base when the optional compare-base fetch fails', async () => { // Why (#6263): a merged MR's target ref may be deleted; a failed compare-base fetch must not drop the worktree onto the default branch. const localRepo = { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1, issueSourcePreference: 'origin' as const } const runtimeStore = { ...store, getRepos: () => [localRepo], getRepo: (id: string) => (id === localRepo.id ? localRepo : undefined) } getGitLabProjectRefForRemoteMock.mockResolvedValue({ host: 'gitlab.example', path: 'group/repo' }) const runtime = new OrcaRuntimeService(runtimeStore as never) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if ( args[0] === 'fetch' && args[2] === '+refs/heads/feature/fix:refs/remotes/origin/feature/fix' ) { return { stdout: '', stderr: '' } } if (args[0] === 'fetch' && args[2] === '+refs/heads/main:refs/remotes/origin/main') { // Target branch was deleted on the remote (merged MR). throw new Error("couldn't find remote ref refs/heads/main") } if (args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'origin/feature/fix') { return { stdout: 'same-repo-mr-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }) gitSpy.mockClear() const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) try { const result = await runtime.resolveManagedMrBase({ repoSelector: 'id:repo-1', mrIid: 79, sourceBranch: 'feature/fix', targetBranch: 'main' }) expect(result).toEqual({ baseBranch: 'origin/feature/fix', pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(result).not.toHaveProperty('compareBaseRef') expect(result).not.toHaveProperty('error') } finally { warnSpy.mockRestore() gitSpy.mockRestore() } }) it('creates the first terminal by id when duplicate repo entries expose the same path', async () => { const runtime = new OrcaRuntimeService(store) const spawn = vi.fn().mockResolvedValue({ id: 'pty-duplicate-path' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), revealTerminalSession: vi.fn().mockResolvedValue({ tabId: 'tab-duplicate-path' }), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) const duplicatePath = '/tmp/workspaces/runtime-duplicate-terminal' const getRepos = vi.spyOn(store, 'getRepos').mockReturnValue([ { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1 }, { id: 'repo-duplicate-entry', path: '/tmp/repo-secondary-worktree', displayName: 'repo-secondary-worktree', badgeColor: 'red', addedAt: 2 } ]) computeWorktreePathMock.mockReturnValue(duplicatePath) ensurePathWithinWorkspaceMock.mockReturnValue(duplicatePath) vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(listWorktrees).mockResolvedValue([ { path: duplicatePath, head: 'def', branch: 'runtime-duplicate-terminal', isBare: false, isMainWorktree: false } ]) try { const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-duplicate-terminal' }) expect(result.warning).toBeUndefined() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: duplicatePath, worktreeId: result.worktree.id }) ) } finally { getRepos.mockRestore() } }) it('resolves an exact path selector when duplicate repo entries expose the same path', async () => { const runtime = new OrcaRuntimeService(store) const duplicatePath = '/tmp/workspaces/runtime-duplicate-selector' const getRepos = vi.spyOn(store, 'getRepos').mockReturnValue([ { id: TEST_REPO_ID, path: TEST_REPO_PATH, displayName: 'repo', badgeColor: 'blue', addedAt: 1 }, { id: 'repo-duplicate-entry', path: '/tmp/repo-secondary-worktree', displayName: 'repo-secondary-worktree', badgeColor: 'red', addedAt: 2 } ]) vi.mocked(listWorktrees).mockResolvedValue([ { path: duplicatePath, head: 'def', branch: 'runtime-duplicate-selector', isBare: false, isMainWorktree: false } ]) try { const worktree = await runtime.showManagedWorktree(`path:${duplicatePath}`) expect(worktree.id).toBe(`${TEST_REPO_ID}::${duplicatePath}`) expect(worktree.path).toBe(duplicatePath) } finally { getRepos.mockRestore() } }) it('keeps CLI-created worktrees successful when initial terminal creation fails', async () => { const runtime = new OrcaRuntimeService(store) const spawn = vi.fn().mockRejectedValue(new Error('pty unavailable')) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) 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(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-terminal-fail') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-terminal-fail') vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(listWorktrees).mockResolvedValue([ { path: '/tmp/workspaces/runtime-terminal-fail', head: 'def', branch: 'runtime-terminal-fail', isBare: false, isMainWorktree: false } ]) try { await expect( runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-terminal-fail' }) ).resolves.toMatchObject({ worktree: expect.objectContaining({ path: '/tmp/workspaces/runtime-terminal-fail' }), warning: 'Failed to create the initial terminal for /tmp/workspaces/runtime-terminal-fail: pty unavailable' }) expect(spawn).toHaveBeenCalled() expect(warn).toHaveBeenCalledWith( '[worktree-create] Failed to create the initial terminal for /tmp/workspaces/runtime-terminal-fail: pty unavailable' ) } finally { warn.mockRestore() } }) it('activates CLI-created worktrees only when explicitly requested', 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(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-activate') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-activate') vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(listWorktrees).mockResolvedValueOnce([ { path: '/tmp/workspaces/runtime-activate', head: 'def', branch: 'runtime-activate', isBare: false, isMainWorktree: false } ]) await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-activate', activate: true }) expect(activateWorktree).toHaveBeenCalledWith( 'repo-1', expect.any(String), undefined, undefined, undefined ) }) it('stamps createdAt alongside lastActivityAt so CLI-created worktrees get the Recent-sort grace window', async () => { // Why: without createdAt, ambient PTY bumps in other worktrees can push the new one below them in Recent sort. 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(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) runtime.attachWindow(1) computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-grace') ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-grace') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: {} }) vi.mocked(listWorktrees).mockResolvedValueOnce([ { path: '/tmp/workspaces/runtime-grace', head: 'def', branch: 'runtime-grace', isBare: false, isMainWorktree: false } ]) const before = Date.now() const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-grace' }) const after = Date.now() expect(result.worktree.createdAt).toBeDefined() expect(result.worktree.createdAt).toBeGreaterThanOrEqual(before) expect(result.worktree.createdAt).toBeLessThanOrEqual(after) // Both fields must share the same now so grace-window math (max(lastActivityAt, createdAt + GRACE_MS)) is well-defined. expect(result.worktree.createdAt).toBe(result.worktree.lastActivityAt) }) it('routes runtime worktree creation through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = new OrcaRuntimeService(runtimeStore as never) const createdWorktree = { path: '/tmp/workspaces/runtime-wsl', head: 'def', branch: 'refs/heads/runtime-wsl', isBare: false, isMainWorktree: false } computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(listWorktrees).mockResolvedValue([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/runtime-wsl^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args[1] === '--path-format=absolute') { return { stdout: `${TEST_REPO_PATH}/.git\n`, stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { return { stdout: 'base-sha\n', stderr: '' } } if (args[0] === 'remote' && args.length === 1) { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'remote' && args[1] === 'get-url') { return { stdout: 'git@github.com:stablyai/orca.git\n', stderr: '' } } return { stdout: '', stderr: '' } }) try { const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'runtime-wsl', pushTarget: { remoteName: 'pr-contributor-orca', branchName: 'contributor/runtime-wsl', remoteUrl: 'git@github.com:contributor/orca.git' } }) expect(result.worktree).toMatchObject({ path: createdWorktree.path, branch: 'refs/heads/runtime-wsl' }) expect(gitSpy).toHaveBeenCalledWith(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { cwd: TEST_REPO_PATH, timeout: 15_000, wslDistro: 'Ubuntu' }) expect(getBranchConflictKind).toHaveBeenCalledWith( TEST_REPO_PATH, 'runtime-wsl', 'origin/main', { wslDistro: 'Ubuntu' } ) expect(getPRForBranchMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'runtime-wsl', null, null, null, { localGitExecOptions: { wslDistro: 'Ubuntu' } } ) expect(addWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, createdWorktree.path, 'runtime-wsl', 'origin/main', false, false, { remoteTrackingBase: { base: 'origin/main', branch: 'main', ref: 'refs/remotes/origin/main', remote: 'origin' }, suggestLocalBaseRefUpdate: true, wslDistro: 'Ubuntu' } ) expect(gitSpy).toHaveBeenCalledWith( ['check-ref-format', '--branch', 'contributor/runtime-wsl'], { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' } ) expect(gitSpy).toHaveBeenCalledWith( [ 'fetch', 'pr-contributor-orca', '+refs/heads/contributor/runtime-wsl:refs/remotes/pr-contributor-orca/contributor/runtime-wsl' ], { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' } ) expect(gitSpy).toHaveBeenCalledWith( [ 'branch', '--set-upstream-to', 'pr-contributor-orca/contributor/runtime-wsl', 'runtime-wsl' ], { cwd: createdWorktree.path, wslDistro: 'Ubuntu' } ) expect(listWorktrees).toHaveBeenCalledWith(TEST_REPO_PATH, { wslDistro: 'Ubuntu' }) } finally { gitSpy.mockRestore() } }) function createWorktreeRemovalRuntime(runtimeStore: unknown = store): OrcaRuntimeService { const emptyPtyProvider = { listProcesses: vi.fn(async () => []), shutdown: vi.fn(async () => {}) } return new OrcaRuntimeService(runtimeStore as never, undefined, { getLocalProvider: () => emptyPtyProvider as never, getSshProvider: () => emptyPtyProvider as never }) } it('skips archive hooks for CLI worktree removal by default', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) vi.mocked(removeWorktree).mockResolvedValue({}) const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID) expect(runHook).not.toHaveBeenCalled() expect(removeWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, TEST_WORKTREE_PATH, false, expect.objectContaining({ knownRemovedWorktree: expect.objectContaining({ path: TEST_WORKTREE_PATH }) }) ) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(TEST_WORKTREE_ID) expect(result.warning).toBe( `orca.yaml archive hook skipped for ${TEST_WORKTREE_PATH}; pass --run-hooks to run it.` ) }) it('does not remove a runtime worktree when watcher teardown cannot release it', async () => { const repo = { ...store.getRepos()[0], symlinkPaths: ['node_modules'] } const runtimeStore = { ...store, getRepos: () => [repo], getRepo: () => repo } const runtime = createWorktreeRemovalRuntime(runtimeStore) vi.mocked(getEffectiveHooks).mockReturnValue(null) deleteWorktreeHistoryDirMock.mockClear() closeLocalWatcherForWorktreePathMock.mockRejectedValue( new Error('file watcher process did not exit after termination deadline') ) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toThrow( 'file watcher process did not exit after termination deadline' ) expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled() }) it('stops worktree PTYs before removing linked paths', async () => { const repo = { ...store.getRepos()[0], symlinkPaths: ['node_modules'] } const runtimeStore = { ...store, getRepos: () => [repo], getRepo: () => repo } const runtime = createWorktreeRemovalRuntime(runtimeStore) const stopAndWait = vi.fn().mockResolvedValue(true) runtime.setPtyController({ write: () => true, kill: () => true, stopAndWait, getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') vi.mocked(removeWorktree).mockResolvedValue({}) removeWorktreeLinkedPathsMock.mockImplementationOnce(async () => { // Destructive teardown must bound the underlying RPCs below the sweep deadline. expect(stopAndWait).toHaveBeenCalledWith( 'pty-1', expect.objectContaining({ deadlineMs: expect.any(Number) }) ) }) await runtime.removeManagedWorktree(TEST_WORKTREE_ID) expect(removeWorktreeLinkedPathsMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH, ['node_modules']) }) it('waits for every watcher layer to settle before restoring after teardown failure', async () => { const runtime = createRuntime() let finishRuntimeClose: () => void = () => {} const runtimeClose = new Promise((resolve) => { finishRuntimeClose = resolve }) const fileCommands = ( runtime as unknown as { fileCommands: { closeFileExplorerWatchersForPath: (path: string) => Promise restoreFileExplorerWatchersAfterFailedRemoval: (path: string) => Promise } } ).fileCommands vi.spyOn(fileCommands, 'closeFileExplorerWatchersForPath').mockReturnValueOnce(runtimeClose) const restoreRuntime = vi .spyOn(fileCommands, 'restoreFileExplorerWatchersAfterFailedRemoval') .mockResolvedValue(undefined) closeLocalWatcherForWorktreePathMock.mockRejectedValueOnce( new Error('desktop watcher close failed') ) const result = runtime.acquireFileWatcherRemoval(TEST_WORKTREE_PATH).catch((error) => error) await Promise.resolve() expect(restoreLocalWatcherAfterFailedRemovalMock).not.toHaveBeenCalled() expect(restoreRuntime).not.toHaveBeenCalled() finishRuntimeClose() await expect(result).resolves.toEqual( expect.objectContaining({ message: 'desktop watcher close failed' }) ) expect(restoreLocalWatcherAfterFailedRemovalMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH) expect(restoreRuntime).toHaveBeenCalledWith(TEST_WORKTREE_PATH, undefined) }) it('restores runtime watchers when CLI worktree deletion fails after teardown', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(removeWorktree).mockRejectedValue(new Error('delete failed')) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toThrow('delete failed') expect(restoreLocalWatcherAfterFailedRemovalMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH) expect(forgetLocalWatcherRemovalSnapshotMock).not.toHaveBeenCalled() const finishRetry = beginWatcherInstall(TEST_WORKTREE_PATH) finishRetry() }) it('closes before and after draining a pre-removal watcher install', async () => { const runtime = createRuntime() const finishInstall = beginWatcherInstall(TEST_WORKTREE_PATH) const acquiring = runtime.acquireFileWatcherRemoval(TEST_WORKTREE_PATH) await vi.waitFor(() => expect(closeLocalWatcherForWorktreePathMock).toHaveBeenCalledTimes(1)) expect(() => beginWatcherInstall(TEST_WORKTREE_PATH)).toThrow( 'cannot start while the worktree is being removed' ) finishInstall() const gate = await acquiring expect(closeLocalWatcherForWorktreePathMock).toHaveBeenCalledTimes(2) await gate.finish(false) expect(restoreLocalWatcherAfterFailedRemovalMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH) const finishRetry = beginWatcherInstall(TEST_WORKTREE_PATH) finishRetry() }) it('recovers forced Windows runtime long-path removal and keeps skipped-hook warnings', async () => { setPlatform('win32') const runtime = createWorktreeRemovalRuntime() await mkdir(TEST_WORKTREE_PATH, { recursive: true }) await writeFile(join(TEST_WORKTREE_PATH, 'scratch.txt'), 'delete me') const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) vi.mocked(removeWorktree).mockRejectedValue( Object.assign(new Error('git worktree remove failed'), { stderr: 'error: failed to delete deep/file.txt: Filename too long' }) ) vi.mocked(listWorktreesStrict) .mockResolvedValueOnce(MOCK_GIT_WORKTREES) .mockResolvedValueOnce(MOCK_GIT_WORKTREES) .mockResolvedValue([]) try { const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID, true) expect(result).toEqual({ preservedBranch: { branchName: 'feature/foo', head: 'abc' }, warning: `orca.yaml archive hook skipped for ${TEST_WORKTREE_PATH}; pass --run-hooks to run it.` }) expect(gitSpy).toHaveBeenCalledWith(['worktree', 'prune'], { cwd: TEST_REPO_PATH }) if (ORIGINAL_PLATFORM === 'win32') { await expect(lstat(TEST_WORKTREE_PATH)).rejects.toMatchObject({ code: 'ENOENT' }) } expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(TEST_WORKTREE_ID) } finally { gitSpy.mockRestore() await rm(TEST_WORKTREE_PATH, { recursive: true, force: true }) } }) it('refuses runtime Windows recovery while Git still reports the row and keeps metadata', async () => { setPlatform('win32') const removeWorktreeMeta = vi.fn() const runtimeStore = { ...store, removeWorktreeMeta } const runtime = createWorktreeRemovalRuntime(runtimeStore) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) const removePathSpy = vi .spyOn(localWorktreeFilesystem, 'removeLocalWorktreePath') .mockResolvedValue(undefined) vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(removeWorktree).mockRejectedValue( Object.assign(new Error('git worktree remove failed'), { stderr: 'error: failed to delete deep/file.txt: Filename too long' }) ) try { await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true)).rejects.toThrow( `Failed to force delete worktree at ${TEST_WORKTREE_PATH}. error: failed to delete deep/file.txt: Filename too long` ) expect(removePathSpy).not.toHaveBeenCalled() expect(gitSpy).not.toHaveBeenCalledWith(['worktree', 'prune'], expect.anything()) expect(removeWorktreeMeta).not.toHaveBeenCalled() } finally { removePathSpy.mockRestore() gitSpy.mockRestore() } }) it('retries stale runtime Git registration cleanup after prior filesystem recovery', async () => { setPlatform('win32') const missingWorktreePath = 'C:\\workspace\\already-removed' const worktreeId = `${TEST_REPO_ID}::${missingWorktreePath}` const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId) const runtime = createWorktreeRemovalRuntime(runtimeStore) const registeredWorktrees = [ { path: TEST_REPO_PATH, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: missingWorktreePath, head: 'abc', branch: 'refs/heads/feature/foo', isBare: false, isMainWorktree: false } ] const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) vi.mocked(listWorktrees).mockResolvedValue(registeredWorktrees) vi.mocked(listWorktreesStrict).mockResolvedValueOnce(registeredWorktrees).mockResolvedValue([]) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) try { const result = await runtime.removeManagedWorktree(worktreeId, true) expect(result).toEqual({ preservedBranch: { branchName: 'feature/foo', head: 'abc' } }) expect(runHook).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() expect(gitSpy).toHaveBeenCalledWith(['worktree', 'prune'], { cwd: TEST_REPO_PATH }) expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId) } finally { gitSpy.mockRestore() } }) it('preserves a locked missing runtime registration even with force', async () => { setPlatform('win32') const missingWorktreePath = 'C:\\workspace\\locked-already-removed' const worktreeId = `${TEST_REPO_ID}::${missingWorktreePath}` const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId) const runtime = createWorktreeRemovalRuntime(runtimeStore) const registeredWorktrees = [ { path: missingWorktreePath, head: 'abc', branch: 'refs/heads/feature/foo', isBare: false, isMainWorktree: false, locked: true, lockReason: 'active agent session' } ] vi.mocked(listWorktreesStrict).mockResolvedValue(registeredWorktrees) vi.mocked(removeWorktree).mockResolvedValue({}) await expect(runtime.removeManagedWorktree(worktreeId, true, false)).rejects.toThrow( 'Worktree is locked by Git. Lock reason: active agent session' ) expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).not.toHaveBeenCalled() }) it('routes runtime worktree removal through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = createWorktreeRemovalRuntime(runtimeStore) vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(removeWorktree).mockResolvedValue({}) await runtime.removeManagedWorktree(TEST_WORKTREE_ID) expect(assertWorktreeCleanForRemoval).toHaveBeenCalledWith(TEST_WORKTREE_PATH, false, { wslDistro: 'Ubuntu' }) expect(removeWorktree).toHaveBeenCalledWith(TEST_REPO_PATH, TEST_WORKTREE_PATH, false, { knownRemovedWorktree: expect.objectContaining({ path: TEST_WORKTREE_PATH }), wslDistro: 'Ubuntu' }) }) it('deletes a Windows runtime worktree using the canonical registered path', async () => { setPlatform('win32') const repo = { id: TEST_REPO_ID, path: 'C:\\repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1 } const requestedWorktreeId = `${TEST_REPO_ID}::C:/workspaces/improve-dashboard` const registeredWorktree = { path: 'c:\\workspaces\\Improve-Dashboard', head: 'feature-head', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } const runtimeStore = { ...store, getRepos: () => [repo], getRepo: (id: string) => (id === TEST_REPO_ID ? repo : undefined), getAllWorktreeMeta: () => ({ [requestedWorktreeId]: makeWorktreeMeta() }), getWorktreeMeta: (worktreeId: string) => worktreeId === requestedWorktreeId ? makeWorktreeMeta() : undefined, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = createWorktreeRemovalRuntime(runtimeStore) vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(listWorktrees).mockResolvedValue([ { path: repo.path, head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, registeredWorktree ]) vi.mocked(listWorktreesStrict).mockResolvedValue([ { path: repo.path, head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, registeredWorktree ]) vi.mocked(removeWorktree).mockResolvedValue({}) await runtime.removeManagedWorktree(requestedWorktreeId) expect(listWorktrees).toHaveBeenCalledWith(repo.path, { wslDistro: 'Ubuntu' }) expect(listWorktreesStrict).toHaveBeenCalledWith(repo.path, { wslDistro: 'Ubuntu' }) expect(assertWorktreeCleanForRemoval).toHaveBeenCalledWith(registeredWorktree.path, false, { wslDistro: 'Ubuntu' }) expect(removeWorktree).toHaveBeenCalledWith(repo.path, registeredWorktree.path, false, { knownRemovedWorktree: registeredWorktree, wslDistro: 'Ubuntu' }) }) it('surfaces selected-runtime list failures during runtime worktree removal', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = createWorktreeRemovalRuntime(runtimeStore) vi.mocked(listWorktrees).mockResolvedValue(MOCK_GIT_WORKTREES) vi.mocked(listWorktreesStrict).mockRejectedValue(new Error('wsl git list failed')) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toThrow( 'wsl git list failed' ) expect(listWorktrees).toHaveBeenCalledWith(TEST_REPO_PATH, { wslDistro: 'Ubuntu' }) expect(listWorktreesStrict).toHaveBeenCalledWith(TEST_REPO_PATH, { wslDistro: 'Ubuntu' }) expect(assertWorktreeCleanForRemoval).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() }) it('force-deletes a branch that was preserved by runtime worktree removal', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(removeWorktree).mockResolvedValue({ preservedBranch: { branchName: 'feature/test', head: 'def456' } }) await runtime.removeManagedWorktree(TEST_WORKTREE_ID) const result = await runtime.forceDeletePreservedBranch( TEST_WORKTREE_ID, 'feature/test', 'def456' ) expect(result).toEqual({ deleted: true }) expect(forceDeleteLocalBranchMock).toHaveBeenCalledWith( TEST_REPO_PATH, 'feature/test', 'def456' ) }) it('force-deletes an SSH branch that was preserved by runtime worktree removal', async () => { const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, path: '/remote/repo', connectionId: 'ssh-1' } const remoteWorktree = { path: '/remote/feature-wt', head: 'def456', branch: 'feature/test', isBare: false, isMainWorktree: false } const remoteWorktreeId = `${remoteRepo.id}::${remoteWorktree.path}` const metaById: Record = { [remoteWorktreeId]: makeWorktreeMeta() } const runtimeStore = { ...store, getRepos: () => [remoteRepo], getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta: (worktreeId: string, meta: Partial) => { metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta } return metaById[worktreeId] }, removeWorktreeMeta: (worktreeId: string) => { delete metaById[worktreeId] } } const provider = { exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }), forceDeletePreservedBranch: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: remoteRepo.path, head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, remoteWorktree ]), removeWorktree: vi.fn().mockResolvedValue({ preservedBranch: { branchName: 'feature/test', head: 'def456' } }) } registerSshGitProvider('ssh-1', provider as never) const runtime = createWorktreeRemovalRuntime(runtimeStore) try { await runtime.removeManagedWorktree(remoteWorktreeId) const result = await runtime.forceDeletePreservedBranch( remoteWorktreeId, 'feature/test', 'def456' ) expect(result).toEqual({ deleted: true }) expect(provider.forceDeletePreservedBranch).toHaveBeenCalledWith( '/remote/repo', 'feature/test', 'def456' ) expect(forceDeleteLocalBranchMock).not.toHaveBeenCalled() } finally { unregisterSshGitProvider('ssh-1') } }) it('routes runtime preserved-branch force-delete through the selected WSL project runtime', async () => { setPlatform('win32') const runtimeStore = { ...store, getProjects: () => [ { id: 'project-1', displayName: 'repo', badgeColor: 'blue', sourceRepoIds: [TEST_REPO_ID], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ], getSettings: () => ({ ...store.getSettings(), localWindowsRuntimeDefault: { kind: 'windows-host' } }) } const runtime = createWorktreeRemovalRuntime(runtimeStore) vi.mocked(removeWorktree).mockResolvedValue({ preservedBranch: { branchName: 'feature/test', head: 'def456' } }) const gitExec = vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) await runtime.removeManagedWorktree(TEST_WORKTREE_ID) await runtime.forceDeletePreservedBranch(TEST_WORKTREE_ID, 'feature/test', 'def456') const runGit = forceDeleteLocalBranchMock.mock.calls[0]?.[3] expect(runGit).toEqual(expect.any(Function)) await runGit?.(['status'], TEST_REPO_PATH) expect(gitExec).toHaveBeenCalledWith(['status'], { cwd: TEST_REPO_PATH, wslDistro: 'Ubuntu' }) }) it('rejects stale preserved-branch runtime cleanup actions with an old head', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(removeWorktree).mockResolvedValue({ preservedBranch: { branchName: 'feature/test', head: 'new456' } }) await runtime.removeManagedWorktree(TEST_WORKTREE_ID) await expect( runtime.forceDeletePreservedBranch(TEST_WORKTREE_ID, 'feature/test', 'old123') ).rejects.toThrow('No preserved branch cleanup is pending') expect(forceDeleteLocalBranchMock).not.toHaveBeenCalled() }) it('coalesces concurrent runtime worktree removals for the same worktree id', async () => { const runtime = createWorktreeRemovalRuntime() const removeStarted = deferred() const finishRemoval = deferred() vi.mocked(removeWorktree).mockImplementation(async () => { removeStarted.resolve() await finishRemoval.promise return {} }) const first = runtime.removeManagedWorktree(TEST_WORKTREE_ID, true) const second = runtime.removeManagedWorktree(TEST_WORKTREE_ID, true) await removeStarted.promise await Promise.resolve() expect(removeWorktree).toHaveBeenCalledTimes(1) finishRemoval.resolve() await expect(Promise.all([first, second])).resolves.toEqual([{}, {}]) }) it('rejects concurrent runtime worktree removals for the same id with different options', async () => { const runtime = createWorktreeRemovalRuntime() const removeStarted = deferred() const finishRemoval = deferred() vi.mocked(removeWorktree).mockImplementation(async () => { removeStarted.resolve() await finishRemoval.promise return {} }) const first = runtime.removeManagedWorktree(TEST_WORKTREE_ID) await removeStarted.promise await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true)).rejects.toThrow( 'Worktree deletion already in progress' ) expect(removeWorktree).toHaveBeenCalledTimes(1) finishRemoval.resolve() await expect(first).resolves.toEqual({}) }) it('treats forced runtime deletion of an already-missing unregistered worktree as cleanup', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-runtime-remove-')) const missingWorktreePath = join(parentDir, 'already-deleted') const worktreeId = `${TEST_REPO_ID}::${missingWorktreePath}` const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId) const runtime = createWorktreeRemovalRuntime(runtimeStore) const notifier = { worktreesChanged: vi.fn() } runtime.setNotifier(notifier as never) try { vi.mocked(listWorktrees).mockResolvedValue([]) await expect(runtime.removeManagedWorktree(worktreeId, true)).resolves.toEqual({}) expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled() expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID) } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('treats normal runtime deletion of an already-missing unregistered worktree as cleanup', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-runtime-remove-')) const missingWorktreePath = join(parentDir, 'already-deleted') const worktreeId = `${TEST_REPO_ID}::${missingWorktreePath}` const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId) const runtime = createWorktreeRemovalRuntime(runtimeStore) const notifier = { worktreesChanged: vi.fn() } runtime.setNotifier(notifier as never) try { vi.mocked(listWorktrees).mockResolvedValue([]) await expect(runtime.removeManagedWorktree(worktreeId)).resolves.toEqual({}) expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled() expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID) } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('force-removes a legacy Orca-created runtime orphaned worktree directory after Git tracking is gone', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-runtime-orphan-')) const repoPath = join(parentDir, 'repo') const orphanPath = join(parentDir, 'orphan') const adminWorktreePath = join(repoPath, '.git', 'worktrees', 'orphan') const worktreeId = `${TEST_REPO_ID}::${orphanPath}` await mkdir(orphanPath, { recursive: true }) await mkdir(adminWorktreePath, { recursive: true }) await writeFile(join(orphanPath, '.git'), `gitdir: ${adminWorktreePath}\n`) await writeFile(join(adminWorktreePath, 'gitdir'), `${join(orphanPath, '.git')}\n`) const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId, { createdAt: Date.now() }) const runtimeStoreWithRepoPath = { ...runtimeStore, getRepos: () => [ { id: TEST_REPO_ID, path: repoPath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 } ], getRepo: (id: string) => id === TEST_REPO_ID ? { id: TEST_REPO_ID, path: repoPath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 } : undefined } const runtime = createWorktreeRemovalRuntime(runtimeStoreWithRepoPath) const notifier = { worktreesChanged: vi.fn() } runtime.setNotifier(notifier as never) try { vi.mocked(listWorktrees).mockResolvedValue([]) await expect(runtime.removeManagedWorktree(worktreeId, true)).resolves.toEqual({}) await expect(lstat(orphanPath)).rejects.toMatchObject({ code: 'ENOENT' }) expect(closeLocalWatcherForWorktreePathMock).toHaveBeenCalledWith(orphanPath) expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled() expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID) } finally { // Why: Windows can keep a just-inspected git admin dir busy briefly. await rm(parentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }) } }) it('prompts then force-removes an Orca-created runtime unregistered leftover directory with no git marker', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-runtime-leftover-')) const repoPath = join(parentDir, 'repo') const leftoverPath = join(parentDir, 'leftover') const worktreeId = `${TEST_REPO_ID}::${leftoverPath}` await mkdir(leftoverPath, { recursive: true }) await writeFile(join(leftoverPath, 'leftover.txt'), 'kept until force\n') const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId, { orcaCreatedAt: Date.now(), orcaCreationSource: 'runtime' }) const runtimeStoreWithRepoPath = { ...runtimeStore, getRepos: () => [ { id: TEST_REPO_ID, path: repoPath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 } ], getRepo: (id: string) => id === TEST_REPO_ID ? { id: TEST_REPO_ID, path: repoPath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 } : undefined } const runtime = createWorktreeRemovalRuntime(runtimeStoreWithRepoPath) const notifier = { worktreesChanged: vi.fn() } runtime.setNotifier(notifier as never) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'status') { throw new Error('fatal: not a git repository') } return { stdout: '', stderr: '' } }) try { vi.mocked(listWorktrees).mockResolvedValue([]) await expect(runtime.removeManagedWorktree(worktreeId)).rejects.toThrow( 'Worktree is no longer registered with Git but its directory remains.' ) await expect(lstat(leftoverPath)).resolves.toBeTruthy() expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).not.toHaveBeenCalled() await expect(runtime.removeManagedWorktree(worktreeId, true)).resolves.toEqual({}) await expect(lstat(leftoverPath)).rejects.toMatchObject({ code: 'ENOENT' }) expect(assertWorktreeCleanForRemoval).not.toHaveBeenCalled() expect(runHook).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled() expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID) expect(gitSpy).toHaveBeenCalledWith(['status', '--short'], { cwd: leftoverPath }) } finally { gitSpy.mockRestore() await rm(parentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }) } }) it('rejects an Orca-created runtime unregistered local directory with a git directory', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-runtime-standalone-')) const repoPath = join(parentDir, 'repo') const standalonePath = join(parentDir, 'standalone') const worktreeId = `${TEST_REPO_ID}::${standalonePath}` await mkdir(join(standalonePath, '.git'), { recursive: true }) const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId, { orcaCreatedAt: Date.now(), orcaCreationSource: 'runtime' }) const runtimeStoreWithRepoPath = { ...runtimeStore, getRepos: () => [ { id: TEST_REPO_ID, path: repoPath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 } ], getRepo: (id: string) => id === TEST_REPO_ID ? { id: TEST_REPO_ID, path: repoPath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 } : undefined } const runtime = createWorktreeRemovalRuntime(runtimeStoreWithRepoPath) try { vi.mocked(listWorktrees).mockResolvedValue([]) await expect(runtime.removeManagedWorktree(worktreeId, true)).rejects.toThrow( `Refusing to delete unregistered worktree path: ${standalonePath}` ) await expect(lstat(standalonePath)).resolves.toBeTruthy() expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).not.toHaveBeenCalled() } finally { await rm(parentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }) } }) it('does not inspect or delete a local path when SSH runtime orphan cleanup has no filesystem provider', async () => { const localPath = await mkdtemp(join(tmpdir(), 'orca-runtime-ssh-missing-fs-')) const repo = { id: 'repo-runtime-ssh-missing-fs', path: '/remote/repo', displayName: 'ssh', badgeColor: 'blue', addedAt: 1, connectionId: 'ssh-missing-fs' } const worktreeId = `${repo.id}::${localPath}` const metaById: Record = { [worktreeId]: makeWorktreeMeta({ orcaCreatedAt: Date.now(), orcaCreationSource: 'ssh' }) } const removeWorktreeMeta = vi.fn((id: string) => { delete metaById[id] }) const runtimeStore = { ...store, getRepos: () => [repo], getRepo: (id: string) => (id === repo.id ? repo : undefined), getAllWorktreeMeta: () => metaById, getWorktreeMeta: (id: string) => metaById[id], setWorktreeMeta: (id: string, meta: Partial) => { metaById[id] = { ...(metaById[id] ?? makeWorktreeMeta()), ...meta } return metaById[id] }, removeWorktreeMeta } const gitProvider = { listWorktrees: vi.fn().mockResolvedValue([ { path: repo.path, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) } registerSshGitProvider(repo.connectionId, gitProvider as never) const runtime = createWorktreeRemovalRuntime(runtimeStore) try { await expect(runtime.removeManagedWorktree(`id:${worktreeId}`, true)).rejects.toThrow( 'SSH filesystem provider unavailable' ) await expect(lstat(localPath)).resolves.toBeTruthy() expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).not.toHaveBeenCalled() } finally { unregisterSshGitProvider(repo.connectionId) await rm(localPath, { recursive: true, force: true }) } }) it('still rejects forced runtime unregistered delete paths that exist on disk', async () => { const existingWorktreePath = await mkdtemp(join(tmpdir(), 'orca-runtime-remove-existing-')) const worktreeId = `${TEST_REPO_ID}::${existingWorktreePath}` const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(worktreeId) const runtime = createWorktreeRemovalRuntime(runtimeStore) try { vi.mocked(listWorktrees).mockResolvedValue([]) await expect(runtime.removeManagedWorktree(worktreeId, true)).rejects.toThrow( 'Refusing to delete unregistered worktree path' ) expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).not.toHaveBeenCalled() } finally { await rm(existingWorktreePath, { recursive: true, force: true }) } }) it('rejects CLI worktree removal when the target contains another registered worktree', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(listWorktrees).mockResolvedValue([ { path: TEST_REPO_PATH, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: TEST_WORKTREE_PATH, head: 'parent', branch: 'refs/heads/parent', isBare: false, isMainWorktree: false }, { path: `${TEST_WORKTREE_PATH}/child`, head: 'child', branch: 'refs/heads/child', isBare: false, isMainWorktree: false } ]) vi.mocked(listWorktreesStrict).mockResolvedValue([ { path: TEST_REPO_PATH, head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: TEST_WORKTREE_PATH, head: 'parent', branch: 'refs/heads/parent', isBare: false, isMainWorktree: false }, { path: `${TEST_WORKTREE_PATH}/child`, head: 'child', branch: 'refs/heads/child', isBare: false, isMainWorktree: false } ]) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, true)).rejects.toThrow( `Refusing to delete worktree because it contains another registered worktree: ${TEST_WORKTREE_PATH}/child` ) expect(runHook).not.toHaveBeenCalled() expect(assertWorktreeCleanForRemoval).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() }) it('fails dirty non-force deletes before PTY teardown', async () => { const runtime = createWorktreeRemovalRuntime() const killSpy = vi.fn().mockReturnValue(true) runtime.setPtyController({ write: () => true, kill: (id) => killSpy(id), getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(assertWorktreeCleanForRemoval).mockRejectedValue( Object.assign(new Error('Worktree has uncommitted or untracked changes.'), { stdout: '?? scratch.txt\n' }) ) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toThrow( `Failed to delete worktree at ${TEST_WORKTREE_PATH}. ?? scratch.txt` ) expect(closeLocalWatcherForWorktreePathMock).not.toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(killSpy).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() }) it('fails locked dirty-force deletes before hooks, link cleanup, or PTY teardown', async () => { const repo = { ...store.getRepos()[0], symlinkPaths: ['node_modules'] } const runtimeStore = { ...store, getRepos: () => [repo], getRepo: () => repo } const runtime = createWorktreeRemovalRuntime(runtimeStore) const killSpy = vi.fn().mockReturnValue(true) runtime.setPtyController({ write: () => true, kill: (id) => killSpy(id), getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) vi.mocked(listWorktreesStrict).mockResolvedValue([ { ...MOCK_GIT_WORKTREES[0], locked: true, lockReason: 'active agent session' } ]) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, true)).rejects.toThrow( `Failed to force delete worktree at ${TEST_WORKTREE_PATH}. Worktree is locked by Git.` ) expect(assertWorktreeCleanForRemoval).not.toHaveBeenCalled() expect(runHook).not.toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(killSpy).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() }) it('rechecks a runtime Git lock after the archive hook before teardown', async () => { const repo = { ...store.getRepos()[0], symlinkPaths: ['node_modules'] } const runtimeStore = { ...store, getRepos: () => [repo], getRepo: () => repo } const runtime = createWorktreeRemovalRuntime(runtimeStore) const killSpy = vi.fn().mockReturnValue(true) runtime.setPtyController({ write: () => true, kill: (id) => killSpy(id), getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) vi.mocked(runHook).mockResolvedValue({ success: true, output: '' }) vi.mocked(listWorktreesStrict) .mockResolvedValueOnce(MOCK_GIT_WORKTREES) .mockResolvedValueOnce([ { ...MOCK_GIT_WORKTREES[0], locked: true, lockReason: 'locked during archive' } ]) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, true)).rejects.toThrow( 'Worktree is locked by Git' ) expect(runHook).toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(assertWorktreeCleanForRemoval).not.toHaveBeenCalled() expect(killSpy).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() }) it('formats preflight subprocess failures and skips PTY teardown', async () => { const runtime = createWorktreeRemovalRuntime() const killSpy = vi.fn().mockReturnValue(true) runtime.setPtyController({ write: () => true, kill: (id) => killSpy(id), getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(assertWorktreeCleanForRemoval).mockRejectedValue( Object.assign(new Error('status failed'), { stderr: 'fatal: unable to read current working directory\n' }) ) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toThrow( `Failed to delete worktree at ${TEST_WORKTREE_PATH}. fatal: unable to read current working directory` ) expect(killSpy).not.toHaveBeenCalled() expect(removeWorktree).not.toHaveBeenCalled() }) it('falls through to orphan cleanup when preflight reports missing/non-repo worktree', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(assertWorktreeCleanForRemoval).mockRejectedValue( Object.assign(new Error('status failed'), { stderr: 'fatal: not a git repository (or any of the parent directories): .git\n' }) ) vi.mocked(removeWorktree).mockRejectedValue( Object.assign(new Error('git worktree remove failed'), { stderr: `fatal: '${TEST_WORKTREE_PATH}' is not a working tree` }) ) vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).resolves.toEqual({}) expect(removeWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, TEST_WORKTREE_PATH, false, expect.objectContaining({ knownRemovedWorktree: expect.objectContaining({ path: TEST_WORKTREE_PATH }) }) ) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(TEST_WORKTREE_ID) }) it('drops the bounded scan cache when orphan-cleanup removal completes', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(getEffectiveHooks).mockReturnValue(null) vi.mocked(assertWorktreeCleanForRemoval).mockRejectedValue( Object.assign(new Error('status failed'), { stderr: 'fatal: not a git repository (or any of the parent directories): .git\n' }) ) vi.mocked(removeWorktree).mockRejectedValue( Object.assign(new Error('git worktree remove failed'), { stderr: `fatal: '${TEST_WORKTREE_PATH}' is not a working tree` }) ) vi.spyOn(gitRunner, 'gitExecFileAsync').mockResolvedValue({ stdout: '', stderr: '' }) // Prime the bounded raw scan cache with the worktree still present. await runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`) await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).resolves.toEqual({}) // Regression (#8882): orphan-cleanup removal must also drop the raw scan cache, or the worktree lingers in listings until the 30s TTL. vi.mocked(listWorktrees).mockResolvedValue([]) await expect(runtime.listDetectedManagedWorktrees(`id:${TEST_REPO_ID}`)).resolves.toMatchObject( { worktrees: [] } ) }) it('runs archive hooks for CLI worktree removal when hooks are explicitly enabled', async () => { const runtime = createWorktreeRemovalRuntime() vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) vi.mocked(runHook).mockResolvedValue({ success: true, output: '' }) vi.mocked(removeWorktree).mockResolvedValue({}) 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 }), undefined, undefined ) expect(removeWorktree).toHaveBeenCalledWith( TEST_REPO_PATH, TEST_WORKTREE_PATH, false, expect.objectContaining({ knownRemovedWorktree: expect.objectContaining({ path: TEST_WORKTREE_PATH }) }) ) }) it('clears optimistic reconcile tokens when a CLI worktree removal succeeds', async () => { const runtime = createWorktreeRemovalRuntime() const worktreeBaseStatus = vi.fn() runtime.setNotifier({ worktreesChanged: vi.fn(), worktreeBaseStatus, reposChanged: vi.fn(), activateWorktree: vi.fn(), createTerminal: vi.fn(), splitTerminal: vi.fn(), renameTerminal: vi.fn(), focusTerminal: vi.fn(), closeTerminal: vi.fn(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: vi.fn() }) vi.mocked(removeWorktree).mockResolvedValue({}) const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) await runtime.removeManagedWorktree(TEST_WORKTREE_ID) await runtime.reconcileWorktreeBaseStatus({ repoId: TEST_REPO_ID, repoPath: TEST_REPO_PATH, worktreeId: TEST_WORKTREE_ID, base: { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' }, branchName: 'feature', createdBaseSha: 'created-sha', token, fetchPromise: Promise.resolve({ ok: true }) }) expect(worktreeBaseStatus).not.toHaveBeenCalled() }) const remoteTrackingBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } function createReconcileRuntime(): { runtime: OrcaRuntimeService worktreeBaseStatus: ReturnType } { const worktreeBaseStatus = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier({ worktreeBaseStatus, worktreeRemoteBranchConflict: vi.fn() } as never) return { runtime, worktreeBaseStatus } } function mockReconcileGit(options: { postFetchSha?: string ancestor?: boolean baseRefMissing?: boolean }) { const { postFetchSha = 'new-base-sha', ancestor = true, baseRefMissing = false } = options return vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args, options) => { const command = args as string[] const cwd = (options as { cwd?: string } | undefined)?.cwd if ( cwd === TEST_REPO_PATH && command[0] === 'rev-parse' && command[1] === '--verify' && command[2] === `${remoteTrackingBase.ref}^{commit}` ) { if (baseRefMissing) { throw new Error('missing base ref') } return { stdout: `${postFetchSha}\n`, stderr: '' } } if (cwd === TEST_REPO_PATH && command[0] === 'merge-base') { if (!ancestor) { throw new Error('not ancestor') } return { stdout: '', stderr: '' } } if (cwd === TEST_REPO_PATH && command[0] === 'rev-list') { return { stdout: '3\n', stderr: '' } } if (cwd === TEST_REPO_PATH && command[0] === 'log') { return { stdout: 'base commit 3\nbase commit 2\n', stderr: '' } } if (cwd === TEST_REPO_PATH && command[0] === 'config') { throw new Error('config missing') } if ( cwd === TEST_REPO_PATH && command[0] === 'rev-parse' && command[1] === '--verify' && command[2] === 'refs/remotes/origin/feature^{commit}' ) { throw new Error('no publish branch conflict') } throw new Error(`unexpected git command: ${command.join(' ')}`) }) } async function reconcileWithToken(runtime: OrcaRuntimeService, token: string): Promise { await runtime.reconcileWorktreeBaseStatus({ repoId: TEST_REPO_ID, repoPath: TEST_REPO_PATH, worktreeId: TEST_WORKTREE_ID, base: remoteTrackingBase, branchName: 'feature', createdBaseSha: 'created-base-sha', token, fetchPromise: Promise.resolve({ ok: true }) }) } it('emits drift without mutating when the fetched base fast-forwards created HEAD', async () => { const { runtime, worktreeBaseStatus } = createReconcileRuntime() const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) const gitSpy = mockReconcileGit({}) try { await reconcileWithToken(runtime, token) expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], { cwd: TEST_WORKTREE_PATH }) expect(worktreeBaseStatus).toHaveBeenCalledWith( expect.objectContaining({ status: 'drift', behind: 3, recentSubjects: ['base commit 3', 'base commit 2'] }) ) } finally { gitSpy.mockRestore() } }) it('emits current when the fetched base still matches created HEAD', async () => { const { runtime, worktreeBaseStatus } = createReconcileRuntime() const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) const gitSpy = mockReconcileGit({ postFetchSha: 'created-base-sha' }) try { await reconcileWithToken(runtime, token) expect(worktreeBaseStatus).toHaveBeenCalledWith({ repoId: TEST_REPO_ID, worktreeId: TEST_WORKTREE_ID, base: 'origin/main', remote: 'origin', status: 'current' }) } finally { gitSpy.mockRestore() } }) it('emits base_changed without mutation when the fetched base rewrote history', async () => { const { runtime, worktreeBaseStatus } = createReconcileRuntime() const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) const gitSpy = mockReconcileGit({ ancestor: false }) try { await reconcileWithToken(runtime, token) expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], { cwd: TEST_WORKTREE_PATH }) expect(worktreeBaseStatus).toHaveBeenCalledWith( expect.objectContaining({ status: 'base_changed' }) ) } finally { gitSpy.mockRestore() } }) it('skips stale-token reconciles without mutating or emitting stale status', async () => { const stale = createReconcileRuntime() const staleToken = stale.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) stale.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) const staleGitSpy = mockReconcileGit({}) try { await reconcileWithToken(stale.runtime, staleToken) expect(stale.worktreeBaseStatus).not.toHaveBeenCalled() expect(staleGitSpy).not.toHaveBeenCalled() } finally { staleGitSpy.mockRestore() } }) it('emits unknown without mutation when fetch fails or the base ref is missing', async () => { const fetchFailure = createReconcileRuntime() const fetchFailureToken = fetchFailure.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) await fetchFailure.runtime.reconcileWorktreeBaseStatus({ repoId: TEST_REPO_ID, repoPath: TEST_REPO_PATH, worktreeId: TEST_WORKTREE_ID, base: remoteTrackingBase, branchName: 'feature', createdBaseSha: 'created-base-sha', token: fetchFailureToken, fetchPromise: Promise.resolve({ ok: false, errorKind: 'git_error' }) }) expect(fetchFailure.worktreeBaseStatus).toHaveBeenCalledWith( expect.objectContaining({ status: 'unknown' }) ) const missingBase = createReconcileRuntime() const missingBaseToken = missingBase.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID) const gitSpy = mockReconcileGit({ baseRefMissing: true }) try { await reconcileWithToken(missingBase.runtime, missingBaseToken) expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], { cwd: TEST_WORKTREE_PATH }) expect(missingBase.worktreeBaseStatus).toHaveBeenCalledWith( expect.objectContaining({ status: 'unknown' }) ) } finally { gitSpy.mockRestore() } }) it('invalidates the filesystem-auth cache after CLI worktree creation', async () => { // Reproduces: stale filesystem-auth cache made git:branchCompare fail with "Access denied" for CLI-created worktree paths. 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(), sleepWorktree: vi.fn(), terminalFitOverrideChanged: vi.fn(), terminalDriverChanged: 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, linkedGitLabMR: meta.linkedGitLabMR ?? existingMeta?.linkedGitLabMR ?? null, linkedGitLabIssue: meta.linkedGitLabIssue ?? existingMeta?.linkedGitLabIssue ?? 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: () => undefined as never, 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', () => { function mockLiveBrowserGuest(): void { electronMocks.webContents.fromId.mockReturnValue({ isDestroyed: () => false }) } it('passes explicit page ids through without resolving the current worktree', async () => { vi.mocked(listWorktrees).mockClear() mockLiveBrowserGuest() 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) 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() mockLiveBrowserGuest() 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 () => { mockLiveBrowserGuest() 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, getRegisteredTabs: vi.fn(() => new Map([['page-2', 2]])) } 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('accepts focus on tab switch without altering bridge args (focus is main-side concern)', async () => { mockLiveBrowserGuest() const runtime = createRuntime() const tabSwitchMock = vi.fn().mockResolvedValue({ switched: 0, browserPageId: 'page-1' }) runtime.setAgentBrowserBridge({ tabSwitch: tabSwitchMock, getRegisteredTabs: vi.fn(() => new Map([['page-1', 1]])) } as never) await expect(runtime.browserTabSwitch({ page: 'page-1', focus: true })).resolves.toEqual({ switched: 0, browserPageId: 'page-1' }) // Bridge is unchanged — focus is delivered to the renderer via IPC, not threaded through bridge state. expect(tabSwitchMock).toHaveBeenCalledWith(undefined, undefined, 'page-1') }) it('does not silently drop invalid explicit worktree selectors for page-targeted commands', async () => { vi.mocked(listWorktrees).mockResolvedValue(MOCK_GIT_WORKTREES) mockLiveBrowserGuest() 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) mockLiveBrowserGuest() 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 () => { mockLiveBrowserGuest() 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 awaits matching PTYs before git', async () => { // Seed the runtime with a live leaf whose worktreeId matches the target. const callOrder: string[] = [] const stopAndWait = vi.fn(async (id: string) => { callOrder.push(`stop-and-wait:${id}`) return true }) const localProvider = createProviderStub(async () => []) vi.mocked(assertWorktreeCleanForRemoval).mockImplementation(async () => { callOrder.push('preflight') }) vi.mocked(removeWorktree).mockImplementation(async () => { callOrder.push('git-removeWorktree') return {} }) const runtime = new OrcaRuntimeService(store, undefined, { getLocalProvider: () => { callOrder.push('getLocalProvider') return localProvider as never } }) runtime.setPtyController({ write: () => true, kill: vi.fn(() => true), stopAndWait, getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') await runtime.removeManagedWorktree(TEST_WORKTREE_ID) // Destructive teardown must bound the underlying RPCs below the sweep deadline. expect(stopAndWait).toHaveBeenCalledWith( 'pty-1', expect.objectContaining({ deadlineMs: expect.any(Number) }) ) // The provider-prefix sweep and the git removal must happen AFTER the // runtime-graph physical stop. Git removal must NOT start before it. const preflightIdx = callOrder.indexOf('preflight') const stopIdx = callOrder.indexOf('stop-and-wait:pty-1') const gitIdx = callOrder.indexOf('git-removeWorktree') expect(preflightIdx).toBeGreaterThanOrEqual(0) expect(stopIdx).toBeGreaterThan(preflightIdx) expect(stopIdx).toBeGreaterThanOrEqual(0) expect(gitIdx).toBeGreaterThan(stopIdx) }) it('does not start Git removal when physical PTY stop cannot be proven', async () => { const localProvider = createProviderStub(async () => []) const runtime = new OrcaRuntimeService(store, undefined, { getLocalProvider: () => localProvider as never }) runtime.setPtyController({ write: () => true, kill: vi.fn(() => true), stopAndWait: vi.fn(async () => false), getForegroundProcess: async () => null }) syncSinglePty(runtime, 'pty-1') await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toThrow( 'Failed to physically stop every PTY' ) expect(removeWorktree).not.toHaveBeenCalled() }) it('thunk resolves the installed provider lazily, not at construction time', async () => { // Simulates the daemon adapter being installed AFTER construction; a capture-at-construction refactor would break this. 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 onPtyStopped = vi.fn() const runtime = new OrcaRuntimeService(store, undefined, { getLocalProvider: () => currentProvider as never, onPtyStopped }) vi.mocked(removeWorktree).mockResolvedValue({}) // 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`, expect.objectContaining({ immediate: true }) ) expect(onPtyStopped).toHaveBeenCalledWith(`${TEST_WORKTREE_ID}@@aaaaaaaa`) // The pre-daemon provider must not have been consulted for the kill. expect(preDaemonProvider.shutdown).not.toHaveBeenCalled() }) }) describe('stale terminal handle resolution (#7718)', () => { function syncSingleTerminalGraph(runtime: OrcaRuntimeService, ptyId: string): void { runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId, paneTitle: 'Terminal 1' } ], mobileSessionTabs: [ { worktree: TEST_WORKTREE_ID, publicationEpoch: 'epoch-1', snapshotVersion: 1, activeGroupId: 'group-1', activeTabId: 'tab-1::pane:1', activeTabType: 'terminal', tabs: [ { type: 'terminal', id: 'tab-1::pane:1', parentTabId: 'tab-1', leafId: 'pane:1', title: 'Terminal 1', isActive: true } ] } ] }) } // Why: client-created terminals (terminal.create) hold LEAF handles that can diverge from the pane's current PTY across reconnects. function issueLeafHandle(runtime: OrcaRuntimeService, ptyId: string): string { const internals = runtime as unknown as { leaves: Map issueHandle: (leaf: unknown) => string } const leaf = Array.from(internals.leaves.values()).find( (candidate) => candidate.ptyId === ptyId ) if (!leaf) { throw new Error('expected leaf record') } return internals.issueHandle(leaf) } it('errors with terminal_handle_stale instead of adopting a replacement PTY', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) syncSingleTerminalGraph(runtime, 'pty-a') const handle = issueLeafHandle(runtime, 'pty-a') // Simulate the pane's PTY being replaced while the remote client still holds the old handle. const internals = runtime as unknown as { leaves: Map } for (const leaf of internals.leaves.values()) { if (leaf.ptyId === 'pty-a') { leaf.ptyId = 'pty-b' } } // The unguarded resolver silently adopts the new PTY — the misroute. expect(runtime.resolveLeafForHandle(handle)).toEqual({ ptyId: 'pty-b' }) // The guarded resolver surfaces the staleness so clients can re-derive. expect(() => runtime.resolveLiveLeafForHandle(handle)).toThrow('terminal_handle_stale') }) it('lets a handle issued before its first PTY adopt that PTY without erroring', async () => { const runtime = new OrcaRuntimeService(store) runtime.attachWindow(1) syncSingleTerminalGraph(runtime, 'pty-a') const handle = issueLeafHandle(runtime, 'pty-a') // Simulate the mobile pre-spawn flow: the handle record predates the PTY (ptyId null); its first PTY must still resolve. const internals = runtime as unknown as { handles: Map } const record = internals.handles.get(handle) if (!record) { throw new Error('expected handle record') } record.ptyId = null expect(runtime.resolveLiveLeafForHandle(handle)).toEqual({ ptyId: 'pty-a' }) }) }) describe('mobile terminal create resilience (#7718)', () => { it('cancels the surface wait without rolling back when the client connection dies', async () => { vi.useFakeTimers() try { const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier(createMobileCreateTestNotifier(closeTerminal)) const abort = new AbortController() const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-abort', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false, signal: abort.signal }) const settled = create.then( () => ({ ok: true as const }), (error: Error) => ({ ok: false as const, error }) ) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) // Client socket dies mid-wait: create must settle right away (not the 10s timeout) and must NOT close the tab (real terminal exists). abort.abort() await vi.advanceTimersByTimeAsync(0) const outcome = await settled expect(outcome.ok).toBe(false) if (outcome.ok === false) { expect(outcome.error.message).toBe('client_disconnected') } expect(closeTerminal).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('keeps a mobile-created terminal alive when its live shell has no registered pane key', async () => { vi.useFakeTimers() try { const closeTerminal = vi.fn() const runtime = new OrcaRuntimeService(store) runtime.setNotifier(createMobileCreateTestNotifier(closeTerminal)) const webContents = { send: vi.fn() } const send = vi.fn((_channel: string, payload: { requestId: string }) => { ipcMain.emit( 'terminal:tabCreateReply', { sender: webContents }, { requestId: payload.requestId, tabId: 'tab-stall', title: 'Terminal' } ) }) webContents.send = send runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false, webContents }) const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { activate: false }) const settled = create.then( () => ({ ok: true as const }), (error: Error) => ({ ok: false as const, error }) ) await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1)) // A live shell backs the tab but its leaf id isn't a terminal UUID, so no pane key registers (stalled renderer publication). runtime.syncWindowGraph(1, { tabs: [], leaves: [ { tabId: 'tab-stall', worktreeId: TEST_WORKTREE_ID, leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-stall', paneTitle: null } ] }) await vi.advanceTimersByTimeAsync(11_000) const outcome = await settled // The create fails, but the timeout must not kill the live terminal (the "tab dies after ~10s" symptom). expect(outcome.ok).toBe(false) if (outcome.ok === false) { expect(outcome.error.message).toContain('Timed out') } expect(closeTerminal).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) }) }) describe('resolveWorktreeScanCacheTtlMs', () => { const BASE_TTL_MS = 30_000 const SCRATCH_TTL_MS = 5 * 60_000 it('keeps the base TTL for ordinary local repos', () => { expect( resolveWorktreeScanCacheTtlMs({ path: '/Users/dev/projects/app', connectionId: '' }) ).toBe(BASE_TTL_MS) }) it('extends the TTL for agent-scratch repo roots', () => { expect( resolveWorktreeScanCacheTtlMs({ path: '/Users/dev/.codex-tmp/foragent-capsule-b1-repo-zP9Az6', connectionId: '' }) ).toBe(SCRATCH_TTL_MS) expect( resolveWorktreeScanCacheTtlMs({ path: '/Users/dev/.claude/skills/obsidian-second-brain', connectionId: '' }) ).toBe(SCRATCH_TTL_MS) }) it('never extends the TTL for SSH repos', () => { // Why: scratch classification reads local path conventions; a remote path // that merely looks similar must keep normal freshness. expect( resolveWorktreeScanCacheTtlMs({ path: '/home/dev/.codex-tmp/capsule', connectionId: 'ssh-1' }) ).toBe(BASE_TTL_MS) }) it('keeps a scratch repo scan cached past the base TTL while normal repos rescan', async () => { // Why: the whole fix lives in the cache-stamp call site; pin the wiring so // a revert to the flat TTL fails CI, not just the pure-function tests. vi.useFakeTimers() // Why: the shared listWorktrees stub keeps call history across this file's // tests; absolute counts need a clean baseline. vi.mocked(listWorktrees).mockClear() try { const scratchPath = '/tmp/.codex-tmp/capsule-a' const runtime = new OrcaRuntimeService({ ...store, getRepos: () => [ { id: 'repo-1', path: '/tmp/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1 }, { id: 'repo-scratch', path: scratchPath, displayName: 'capsule', badgeColor: 'blue', addedAt: 1 } ] } as never) const internals = runtime as unknown as { listResolvedWorktrees: () => Promise } const scanCallsFor = (path: string): number => vi.mocked(listWorktrees).mock.calls.filter((call) => call[0] === path).length await internals.listResolvedWorktrees() expect(scanCallsFor('/tmp/repo')).toBe(1) expect(scanCallsFor(scratchPath)).toBe(1) vi.advanceTimersByTime(BASE_TTL_MS + 1_000) await internals.listResolvedWorktrees() expect(scanCallsFor('/tmp/repo')).toBe(2) expect(scanCallsFor(scratchPath)).toBe(1) vi.advanceTimersByTime(SCRATCH_TTL_MS) await internals.listResolvedWorktrees() expect(scanCallsFor(scratchPath)).toBe(2) } finally { vi.useRealTimers() } }) })