From 6448b2fda9fe384ff6747357743f54243b6c09dd Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:12:16 -0700 Subject: [PATCH] refactor(runtime): check workspace startup on original prototypes (#27767) --- config/ts-nocheck-baseline.txt | 1 - .../runtime-availability.spec.ts | 57 +++++ ...worktree-provisioning-availability.test.ts | 242 ++++++++++++++++++ .../workspace-terminal-provisioning.ts} | 40 ++- .../runtime/workspaces/workspace-creation.ts | 15 +- src/main/worktree-create-preparation.ts | 14 +- 6 files changed, 341 insertions(+), 28 deletions(-) rename src/main/runtime/{orca-runtime-get-worktree-terminal-provisioning-host.ts => workspaces/startup/workspace-terminal-provisioning.ts} (65%) diff --git a/config/ts-nocheck-baseline.txt b/config/ts-nocheck-baseline.txt index 63cd9eebdcf..6826e902c63 100644 --- a/config/ts-nocheck-baseline.txt +++ b/config/ts-nocheck-baseline.txt @@ -16,7 +16,6 @@ src/main/runtime/orca-runtime-focus-terminal.ts src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts src/main/runtime/orca-runtime-get-runtime-id.ts src/main/runtime/orca-runtime-get-worktree-ps.ts -src/main/runtime/orca-runtime-get-worktree-terminal-provisioning-host.ts src/main/runtime/orca-runtime-has-exact-persisted-terminal-surface-identity.ts src/main/runtime/orca-runtime-has-live-or-persisted-serve-or-ssh-owned-pty-binding.ts src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts diff --git a/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts b/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts index 167db4e6f7a..0663ab3db36 100644 --- a/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts +++ b/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts @@ -14,6 +14,11 @@ import { makeWorkspaceSessionWithHeadlessTerminal, store } from '../orca-runtime-test-fixtures.spec' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Store } from '../../persistence/loading-store/store' +import { ProfileStateSqliteAuthority } from '../../persistence/profile-state/profile-state-sqlite-authority' describe('OrcaRuntimeService', () => { it('starts unavailable with no authoritative window', () => { @@ -348,3 +353,55 @@ describe('OrcaRuntimeService', () => { }) }) }) + +it('captures the root repo resolver before reading the selector argument', async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-prefetch-dispatch-')) + const authority = new ProfileStateSqliteAuthority( + join(directory, 'profile-state.db'), + 'prefetch-dispatch' + ) + const store = new Store({ + dataFile: join(directory, 'orca-data.json'), + profileStateAuthority: authority + }) + + try { + const runtime = new OrcaRuntimeService(store) + const events: string[] = [] + const original = vi.fn(function (this: typeof runtime, selector: string) { + expect(this).toBe(runtime) + expect(arguments.length).toBe(1) + expect(selector).toBe('repo') + events.push('original resolver') + throw new Error('original resolver') + }) + const replacement = vi.fn(() => { + throw new Error('replacement resolver') + }) + Object.defineProperty(runtime, 'resolveRepoSelector', { + configurable: true, + get: () => { + events.push('resolver read') + return original + } + }) + + await expect( + runtime.prefetchManagedWorktreeCreateBase({ + get repoSelector() { + events.push('selector read') + Object.defineProperty(runtime, 'resolveRepoSelector', { value: replacement }) + return 'repo' + } + }) + ).rejects.toThrow('original resolver') + + expect(events).toEqual(['resolver read', 'selector read', 'original resolver']) + expect(original).toHaveBeenCalledOnce() + expect(replacement).not.toHaveBeenCalled() + } finally { + store.freezeWrites() + authority.close() + rmSync(directory, { recursive: true, force: true }) + } +}) diff --git a/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts b/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts index f98829aff27..2b14cbb6b21 100644 --- a/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts +++ b/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts @@ -12,6 +12,10 @@ import { } from './orca-runtime-test-mocks.spec' import { store } from './orca-runtime-test-fixtures.spec' import { createMobileCreateTestNotifier } from './orca-runtime-test-scenario-builders.spec' +import { tmpdir } from 'node:os' +import type { Repo } from '../../shared/repo-types' +import type { Worktree } from '../../shared/worktree/types' +import type { RuntimePtyController } from './runtime-pty-controller-contract' describe('host worktree creation while its renderer is unavailable', () => { it.each([ @@ -92,3 +96,241 @@ describe('host worktree creation while its renderer is unavailable', () => { } ) }) + +const repo: Repo = { + id: 'construction-repo', + path: tmpdir(), + displayName: 'Construction', + badgeColor: 'blue', + addedAt: 1 +} +class QueryRuntime extends OrcaRuntimeService { + checkDraft() { + return this.buildStartupForDraft(repo, 'startup draft') + } + checkAgentGuard() { + expect(() => this.buildStartupForAgent(repo, 'claude', undefined)).toThrow( + 'runtime_unavailable' + ) + } + checkLineage() { + return this.recordCreatedWorktreeLineage( + { id: `construction-repo::${tmpdir()}`, instanceId: 'construction-instance' }, + { kind: 'none', warnings: [] } + ) + } + checkBackground() { + return this.shouldProvisionWorktreeInBackground() + } + checkProvisioningHost() { + return Object.keys(this.getWorktreeTerminalProvisioningHost()) + } + checkReadinessHost() { + return Object.keys(this.getWorktreeStartupReadinessHost()) + } +} +const cases = [ + { + name: 'public draft-agent null-store query', + run: (r: QueryRuntime) => r.resolveStartupDraftAgent(repo), + check: async (v: unknown) => { + await expect(v).resolves.toBeNull() + } + }, + { + name: 'protected draft-plan null-store query', + run: (r: QueryRuntime) => r.checkDraft(), + check: async (v: unknown) => { + await expect(v).resolves.toBeNull() + } + }, + { + name: 'protected agent-plan original null-store error', + run: (r: QueryRuntime) => r.checkAgentGuard(), + check: async (v: unknown) => { + expect(v).toBeUndefined() + } + }, + { + name: 'lineage-none projection with valid identities', + run: (r: QueryRuntime) => r.checkLineage(), + check: async (v: unknown) => { + expect(v).toEqual({ lineage: null, workspaceLineage: null, warnings: [] }) + } + }, + { + name: 'public base-prefetch original async null-store error', + run: (r: QueryRuntime) => r.prefetchManagedWorktreeCreateBase({ repoSelector: repo.id }), + check: async (v: unknown) => { + await expect(v).rejects.toThrow('runtime_unavailable') + } + }, + { + name: 'background provisioning pure readiness query', + run: (r: QueryRuntime) => r.checkBackground(), + check: async (v: unknown) => { + expect(v).toBe(true) + } + }, + { + name: 'lazy terminal provisioning capability object', + run: (r: QueryRuntime) => r.checkProvisioningHost(), + check: async (v: unknown) => { + expect(v).toEqual([ + 'canSpawn', + 'createTerminal', + 'splitTerminal', + 'setTabTitle', + 'setTabColor', + 'getSettings', + 'getPtyId', + 'recordSetupCompletionToken' + ]) + } + }, + { + name: 'lazy startup-readiness capability object', + run: (r: QueryRuntime) => r.checkReadinessHost(), + check: async (v: unknown) => { + expect(v).toEqual([ + 'getPtyId', + 'getForegroundProcess', + 'hasChildProcesses', + 'subscribeToData', + 'readRecentOutput', + 'write' + ]) + } + }, + { + name: 'execution-host metadata query permits initial absent record', + run: (r: QueryRuntime) => r.launchedAgentHostProvesAgent('startup-pty'), + check: async (v: unknown) => { + expect(v).toBe(process.platform !== 'win32') + } + } +] +for (const test of cases) { + it(`preserves ${test.name} at existing constructor getter seam`, async () => { + let result: unknown + class ConstructionRuntime extends QueryRuntime { + protected get canonicalFetchKeyCache(): ReadonlyMap { + result = test.run(this) + return new Map() + } + } + expect(() => new ConstructionRuntime(null)).not.toThrow() + await test.check(result) + }) +} + +describe('startup hosts retain current runtime capabilities', () => { + it('reads current create and split overrides with the captured worktree and three arguments', async () => { + const runtime = new OrcaRuntimeService(store) + onTestFinished(() => { + vi.restoreAllMocks() + }) + const created: Worktree = { + id: 'created-worktree', + repoId: 'repo-1', + displayName: 'Created', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + path: tmpdir(), + head: 'abc', + branch: 'created', + isBare: false, + isMainWorktree: false + } + const host = runtime['getWorktreeTerminalProvisioningHost'](created) + const options = { command: 'echo startup' } + const create = vi.spyOn(runtime, 'createTerminal').mockImplementation(async function ( + this: OrcaRuntimeService, + selector, + input, + evidence + ) { + expect(this).toBe(runtime) + expect(arguments.length).toBe(3) + expect([selector, input, evidence]).toEqual(['id:created-worktree', options, created]) + expect(evidence).toBe(created) + return { handle: 'created-handle', worktreeId: created.id, title: null } + }) + const split = vi.spyOn(runtime, 'splitTerminal').mockImplementation(async function ( + this: OrcaRuntimeService, + handle, + input, + evidence + ) { + expect(this).toBe(runtime) + expect(arguments.length).toBe(3) + expect([handle, input, evidence]).toEqual(['created-handle', options, created]) + expect(evidence).toBe(created) + return { handle: 'split-handle', tabId: 'tab', paneRuntimeId: 1 } + }) + await host.createTerminal('id:created-worktree', options) + await host.splitTerminal('created-handle', options) + expect(create).toHaveBeenCalledOnce() + expect(split).toHaveBeenCalledOnce() + }) + + it('reads replacement providers and preserves readiness failures after removal', async () => { + const runtime = new OrcaRuntimeService(store) + const host = runtime['getWorktreeStartupReadinessHost']() + const hasChildProcesses = host.hasChildProcesses + if (!hasChildProcesses) { + throw new Error('missing readiness child-process callback') + } + const provisioning = runtime['getWorktreeTerminalProvisioningHost']() + expect(provisioning.canSpawn()).toBe(false) + const controller: RuntimePtyController = { + write: () => true, + kill: () => true, + getForegroundProcess: vi.fn(async function (this: RuntimePtyController, ptyId: string) { + expect(this).toBe(controller) + expect(arguments.length).toBe(1) + expect(ptyId).toBe('startup-pty') + return 'late provider' + }) + } + runtime.setPtyController(controller) + await expect(host.getForegroundProcess('startup-pty')).resolves.toBe('late provider') + await expect(hasChildProcesses('startup-pty')).resolves.toBe(false) + const replacement: RuntimePtyController = { + write: () => true, + kill: () => true, + spawn: async () => ({ id: 'spawned' }), + getForegroundProcess: async () => 'replacement provider', + hasChildProcesses: async () => true + } + runtime.setPtyController(replacement) + expect(provisioning.canSpawn()).toBe(true) + await expect(host.getForegroundProcess('startup-pty')).resolves.toBe('replacement provider') + await expect(hasChildProcesses('startup-pty')).resolves.toBe(true) + runtime.setPtyController(null) + expect(provisioning.canSpawn()).toBe(false) + expect(() => host.getForegroundProcess('startup-pty')).toThrow(TypeError) + expect(() => hasChildProcesses('startup-pty')).toThrow(TypeError) + }) + + it('reads the current store and canonical setup-token map when invoked', () => { + const runtime = new OrcaRuntimeService(store) + const host = runtime['getWorktreeTerminalProvisioningHost']() + const original = runtime['setupCompletionTokenByPtyId'] + const replacement = new Map() + const settings = { ...store.getSettings(), workspaceDir: tmpdir() } + Object.defineProperty(runtime, 'setupCompletionTokenByPtyId', { value: replacement }) + Object.defineProperty(runtime, 'store', { value: { ...store, getSettings: () => settings } }) + host.recordSetupCompletionToken?.('startup-pty', 'late-token') + expect(replacement.get('startup-pty')).toBe('late-token') + expect(original.has('startup-pty')).toBe(false) + expect(host.getSettings()).toBe(settings) + }) +}) diff --git a/src/main/runtime/orca-runtime-get-worktree-terminal-provisioning-host.ts b/src/main/runtime/workspaces/startup/workspace-terminal-provisioning.ts similarity index 65% rename from src/main/runtime/orca-runtime-get-worktree-terminal-provisioning-host.ts rename to src/main/runtime/workspaces/startup/workspace-terminal-provisioning.ts index 5a4906f8801..6c0e3c67d5d 100644 --- a/src/main/runtime/orca-runtime-get-worktree-terminal-provisioning-host.ts +++ b/src/main/runtime/workspaces/startup/workspace-terminal-provisioning.ts @@ -1,17 +1,37 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithActivateManagedWorktree } from './workspaces/workspace-activation' -import type { WorktreeTerminalProvisioningHost } from './runtime-worktree-terminal-provisioning' -import type { WorktreeStartupReadinessHost } from './runtime-worktree-startup-readiness' -import { prefetchWorktreeCreateBase } from '../worktree-create-base-prefetch' -import { prepareWorktreeCreateForRepo } from '../worktree-create-preparation' -import { getWorktreeCreatePrefetchGitOptions } from '../project-runtime-git-options' -import type { Worktree } from '../../shared/worktree/types' +import type { RuntimeRemoteFetchController } from '../../runtime-remote-fetch-controller' +import type { TerminalSplitOptions } from '../../terminal/launch/terminal-split-surface' +import type { RuntimeTerminalSplit } from '../../../../shared/runtime-terminal-contracts' +import { OrcaRuntimeWithActivateManagedWorktree } from '../workspace-activation' +import type { WorktreeTerminalProvisioningHost } from '../../runtime-worktree-terminal-provisioning' +import type { WorktreeStartupReadinessHost } from '../../runtime-worktree-startup-readiness' +import { prefetchWorktreeCreateBase } from '../../../worktree-create-base-prefetch' +import { prepareWorktreeCreateForRepo } from '../../../worktree-create-preparation' +import { getWorktreeCreatePrefetchGitOptions } from '../../../project-runtime-git-options' +import type { Worktree } from '../../../../shared/worktree/types' import { navigationTargetsHost, type RuntimeNavigationTarget -} from '../../shared/runtime-navigation' +} from '../../../../shared/runtime-navigation' + +export abstract class OrcaRuntimeWithGetWorktreeTerminalProvisioningHost extends OrcaRuntimeWithActivateManagedWorktree { + abstract splitTerminal( + handle: string, + opts?: TerminalSplitOptions, + createdWorktree?: Worktree + ): Promise + abstract resolveRemoteTrackingBase( + ...args: Parameters + ): ReturnType + abstract hasRemoteTrackingRef( + ...args: Parameters + ): ReturnType + abstract getOrStartRemoteTrackingBaseRefresh( + ...args: Parameters + ): ReturnType + abstract fetchRemoteWithCache( + ...args: Parameters + ): ReturnType -export class OrcaRuntimeWithGetWorktreeTerminalProvisioningHost extends OrcaRuntimeWithActivateManagedWorktree { protected shouldProvisionWorktreeInBackground(navigation?: RuntimeNavigationTarget): boolean { return ( navigationTargetsHost(navigation ?? 'host') && diff --git a/src/main/runtime/workspaces/workspace-creation.ts b/src/main/runtime/workspaces/workspace-creation.ts index 4dbb9200dbc..6e8dc96678c 100644 --- a/src/main/runtime/workspaces/workspace-creation.ts +++ b/src/main/runtime/workspaces/workspace-creation.ts @@ -1,10 +1,9 @@ -import type { RuntimeRemoteFetchController } from '../runtime-remote-fetch-controller' import type { Repo } from '../../../shared/repo-types' import type { WorktreeLineageInput, WorktreeLineageResolution } from '../runtime-worktree-lineage-resolution' -import { OrcaRuntimeWithGetWorktreeTerminalProvisioningHost } from '../orca-runtime-get-worktree-terminal-provisioning-host' +import { OrcaRuntimeWithGetWorktreeTerminalProvisioningHost } from './startup/workspace-terminal-provisioning' import type { RuntimeManagedWorktreeCreateArgs } from '../runtime-managed-worktree-create-types' import type { CreateWorktreeResult } from '../../../shared/worktree/create-types' import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' @@ -29,18 +28,6 @@ export abstract class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWi input?: WorktreeLineageInput, childRepo?: Repo ): Promise - abstract resolveRemoteTrackingBase( - ...args: Parameters - ): ReturnType - abstract hasRemoteTrackingRef( - ...args: Parameters - ): ReturnType - abstract getOrStartRemoteTrackingBaseRefresh( - ...args: Parameters - ): ReturnType - abstract fetchRemoteWithCache( - ...args: Parameters - ): ReturnType async createManagedWorktree( args: RuntimeManagedWorktreeCreateArgs diff --git a/src/main/worktree-create-preparation.ts b/src/main/worktree-create-preparation.ts index 5ca98be283b..79313504795 100644 --- a/src/main/worktree-create-preparation.ts +++ b/src/main/worktree-create-preparation.ts @@ -1,7 +1,7 @@ import { worktreePreparationGit } from './git/worktree-create-git-executor' import { mkdir } from 'node:fs/promises' import { posix, win32 } from 'node:path' -import type { Store } from './persistence' +import type { ProjectRuntimeResolutionStore } from './local-project-runtime-resolution' import type { Repo } from '../shared/repo-types' import { isFolderRepo } from '../shared/repo-kind' import { isWindowsAbsolutePathLike } from '../shared/cross-platform-path' @@ -93,8 +93,16 @@ function timePhase( return args.timing ? args.timing.time(phase, operation) : operation() } +export type WorktreePreparationStore = Omit & { + getSettings(): Pick< + Parameters[1], + 'workspaceDir' | 'nestWorkspaces' + > & + ReturnType> +} + export function prepareWorktreeCreateForRepo( - store: Store, + store: WorktreePreparationStore, repo: Repo, baseBranch: string, beforeMaterialization?: Promise @@ -105,7 +113,7 @@ export function prepareWorktreeCreateForRepo( } async function prepareWorktreeCreateInBackground( - store: Store, + store: WorktreePreparationStore, repo: Repo, baseBranch: string, beforeMaterialization?: Promise