mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(orchestration): fail loudly when a startupAgent launch cannot resolve
The guard ordering let a caller that set startupAgent together with a caller-supplied launch (env/launchConfig/launchAgent/startupCommandDelivery/ claudeAgentTeamsSourceCommand), a raw command, a resume session, or that hit an unavailable store, fall through with an unresolved command and spawn a bare shell — the exact silent timeout at agent_readiness that startupAgent exists to prevent. No current caller does this, so it was latent rather than live. Those combinations now throw instead. A raw `command` counts as a contradiction because it would be silently overwritten by the built launch, and `resumeProviderSession` because it survives to spawn and would pair resume identity with a freshly built fresh-launch command. The non-startupAgent path is unchanged: verified bit-for-bit over all 256 combinations of the eight guard inputs. Also covers the repo-less folder-workspace branch with a test, which the new isRemote fallback made reachable for the first time.
This commit is contained in:
@@ -12614,6 +12614,76 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(spawnCall?.command).toMatch(/^cursor-agent(\s|$)/)
|
||||
})
|
||||
|
||||
// Why: folder workspaces have no repo, so command sniffing skipped them entirely
|
||||
// and spawned the bare string; an explicit agent must still resolve.
|
||||
it('resolves a startupAgent in a repo-less folder workspace', async () => {
|
||||
const folderPath = await mkdtemp(join(tmpdir(), 'orca-runtime-folder-startup-agent-'))
|
||||
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
|
||||
const folderWorkspace = makeFolderWorkspace({ folderPath })
|
||||
const projectGroup = makeFolderProjectGroup({ parentPath: folderPath })
|
||||
const runtime = new OrcaRuntimeService({
|
||||
...createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup),
|
||||
getSettings: () => ({
|
||||
...store.getSettings(),
|
||||
disabledTuiAgents: [],
|
||||
agentCmdOverrides: {},
|
||||
agentDefaultArgs: { cursor: '--force' },
|
||||
agentDefaultEnv: {}
|
||||
})
|
||||
} as never)
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
|
||||
await runtime.createTerminal(`id:${TEST_FOLDER_WORKSPACE_KEY}`, { startupAgent: 'cursor' })
|
||||
|
||||
const spawnCall = spawn.mock.calls[0]?.[0] as
|
||||
| { command?: string; launchAgent?: string }
|
||||
| undefined
|
||||
expect(spawnCall?.command).toBe("cursor-agent '--force'")
|
||||
expect(spawnCall?.launchAgent).toBe('cursor')
|
||||
})
|
||||
|
||||
// Why: silently returning the caller's opts would spawn a bare shell that can
|
||||
// only time out at agent readiness — the failure startupAgent exists to stop.
|
||||
it('rejects a startupAgent create that also supplies its own launch', async () => {
|
||||
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
|
||||
const runtime = new OrcaRuntimeService({
|
||||
...store,
|
||||
getSettings: () => ({
|
||||
...store.getSettings(),
|
||||
disabledTuiAgents: [],
|
||||
agentCmdOverrides: {}
|
||||
})
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
|
||||
for (const conflicting of [
|
||||
{ env: { SOME_VAR: 'set' } },
|
||||
// Why: a raw command would be silently overwritten by the built launch.
|
||||
{ command: 'cursor-agent --resume' },
|
||||
// Why: resume identity paired with a freshly built launch is incoherent.
|
||||
{ resumeProviderSession: { key: 'session_id', id: 'prior-session' } as never },
|
||||
{ launchAgent: 'cursor' as const }
|
||||
]) {
|
||||
await expect(
|
||||
runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
|
||||
startupAgent: 'cursor',
|
||||
...conflicting
|
||||
})
|
||||
).rejects.toThrow(/cannot combine/)
|
||||
}
|
||||
expect(spawn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a startupAgent create for a disabled agent', async () => {
|
||||
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
|
||||
const runtimeStore = {
|
||||
|
||||
@@ -23876,23 +23876,34 @@ export class OrcaRuntimeService {
|
||||
): Promise<TerminalCreateOptions> {
|
||||
// Why: raw shell commands like `codex exec` must remain user-authored shell.
|
||||
// Only unmanaged, repo-backed, bare agent launches get Settings defaults.
|
||||
if (
|
||||
const callerSuppliedLaunch =
|
||||
opts.env ||
|
||||
opts.launchConfig ||
|
||||
opts.launchAgent ||
|
||||
opts.startupCommandDelivery ||
|
||||
opts.claudeAgentTeamsSourceCommand ||
|
||||
!this.store
|
||||
) {
|
||||
return opts
|
||||
}
|
||||
// An explicit startupAgent is already the agent's identity, so it needs no
|
||||
// command sniffing and works for repo-less folder workspaces too.
|
||||
if (!opts.startupAgent && (!opts.command || !workspace.repo)) {
|
||||
opts.claudeAgentTeamsSourceCommand
|
||||
const store = this.store
|
||||
if (opts.startupAgent) {
|
||||
// Why: returning opts unresolved here would spawn a bare shell that can only
|
||||
// time out waiting for an agent — the silent failure startupAgent exists to
|
||||
// prevent. An explicit agent needs no command sniffing and no repo, so the
|
||||
// only unresolvable cases are a contradictory caller or a dead runtime.
|
||||
// `command` and `resumeProviderSession` count as contradictions too: the
|
||||
// first would be silently overwritten, the second would pair resume identity
|
||||
// with a freshly built launch.
|
||||
if (callerSuppliedLaunch || opts.command || opts.resumeProviderSession) {
|
||||
throw new Error(
|
||||
`startupAgent ${opts.startupAgent} cannot combine with a caller-supplied launch.`
|
||||
)
|
||||
}
|
||||
if (!store) {
|
||||
throw new Error('runtime_unavailable')
|
||||
}
|
||||
} else if (callerSuppliedLaunch || !store || !opts.command || !workspace.repo) {
|
||||
return opts
|
||||
}
|
||||
|
||||
const settings = this.store.getSettings()
|
||||
const settings = store.getSettings()
|
||||
const platform = this.getAgentLaunchPlatformForWorkspace(workspace)
|
||||
const isRemote = workspace.repo ? repoIsRemote(workspace.repo) : Boolean(workspace.connectionId)
|
||||
const queuedShell = resolveLocalWindowsAgentStartupShell({
|
||||
|
||||
Reference in New Issue
Block a user