mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
refactor(runtime): check workspace startup on original prototypes (#27767)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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<string, string> {
|
||||
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<string, string>()
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
+30
-10
@@ -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<RuntimeTerminalSplit>
|
||||
abstract resolveRemoteTrackingBase(
|
||||
...args: Parameters<RuntimeRemoteFetchController['resolveRemoteTrackingBase']>
|
||||
): ReturnType<RuntimeRemoteFetchController['resolveRemoteTrackingBase']>
|
||||
abstract hasRemoteTrackingRef(
|
||||
...args: Parameters<RuntimeRemoteFetchController['hasRemoteTrackingRef']>
|
||||
): ReturnType<RuntimeRemoteFetchController['hasRemoteTrackingRef']>
|
||||
abstract getOrStartRemoteTrackingBaseRefresh(
|
||||
...args: Parameters<RuntimeRemoteFetchController['getOrStartRemoteTrackingBaseRefresh']>
|
||||
): ReturnType<RuntimeRemoteFetchController['getOrStartRemoteTrackingBaseRefresh']>
|
||||
abstract fetchRemoteWithCache(
|
||||
...args: Parameters<RuntimeRemoteFetchController['fetchRemoteWithCache']>
|
||||
): ReturnType<RuntimeRemoteFetchController['fetchRemoteWithCache']>
|
||||
|
||||
export class OrcaRuntimeWithGetWorktreeTerminalProvisioningHost extends OrcaRuntimeWithActivateManagedWorktree {
|
||||
protected shouldProvisionWorktreeInBackground(navigation?: RuntimeNavigationTarget): boolean {
|
||||
return (
|
||||
navigationTargetsHost(navigation ?? 'host') &&
|
||||
@@ -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<WorktreeLineageResolution>
|
||||
abstract resolveRemoteTrackingBase(
|
||||
...args: Parameters<RuntimeRemoteFetchController['resolveRemoteTrackingBase']>
|
||||
): ReturnType<RuntimeRemoteFetchController['resolveRemoteTrackingBase']>
|
||||
abstract hasRemoteTrackingRef(
|
||||
...args: Parameters<RuntimeRemoteFetchController['hasRemoteTrackingRef']>
|
||||
): ReturnType<RuntimeRemoteFetchController['hasRemoteTrackingRef']>
|
||||
abstract getOrStartRemoteTrackingBaseRefresh(
|
||||
...args: Parameters<RuntimeRemoteFetchController['getOrStartRemoteTrackingBaseRefresh']>
|
||||
): ReturnType<RuntimeRemoteFetchController['getOrStartRemoteTrackingBaseRefresh']>
|
||||
abstract fetchRemoteWithCache(
|
||||
...args: Parameters<RuntimeRemoteFetchController['fetchRemoteWithCache']>
|
||||
): ReturnType<RuntimeRemoteFetchController['fetchRemoteWithCache']>
|
||||
|
||||
async createManagedWorktree(
|
||||
args: RuntimeManagedWorktreeCreateArgs
|
||||
|
||||
@@ -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<T>(
|
||||
return args.timing ? args.timing.time(phase, operation) : operation()
|
||||
}
|
||||
|
||||
export type WorktreePreparationStore = Omit<ProjectRuntimeResolutionStore, 'getSettings'> & {
|
||||
getSettings(): Pick<
|
||||
Parameters<typeof getWorktreePathSettings>[1],
|
||||
'workspaceDir' | 'nestWorkspaces'
|
||||
> &
|
||||
ReturnType<NonNullable<ProjectRuntimeResolutionStore['getSettings']>>
|
||||
}
|
||||
|
||||
export function prepareWorktreeCreateForRepo(
|
||||
store: Store,
|
||||
store: WorktreePreparationStore,
|
||||
repo: Repo,
|
||||
baseBranch: string,
|
||||
beforeMaterialization?: Promise<void>
|
||||
@@ -105,7 +113,7 @@ export function prepareWorktreeCreateForRepo(
|
||||
}
|
||||
|
||||
async function prepareWorktreeCreateInBackground(
|
||||
store: Store,
|
||||
store: WorktreePreparationStore,
|
||||
repo: Repo,
|
||||
baseBranch: string,
|
||||
beforeMaterialization?: Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user