mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
* fix(orchestration): let worker-start actually produce a structured chat `orchestration.workerStart` reads the user's "open agent tabs in chat" default, but two placement checks downgraded a structured-preferring worker to a PTY terminal agent for the two flags a routine dispatch always passes: --worktree new-child / new-top-level -> worktree_creation --model / --effort -> launch_preferences so in practice a structured worker never happened. launch_preferences was stale. PR #19040 gave AgentSessionAttachParams `options` and added resolveStructuredLaunchSeedOptions, which narrows a saved selection to exactly `model` and `effort` — the two ids both structured providers accept as strings. --model/--effort now go through that same narrowing (extracted as narrowStructuredLaunchSeedOptions) and seed the worker's session instead of forcing a terminal. An option set that narrows to nothing resolves to undefined, never `{}`, which would fail the record's bounded-string guard under a code that is not a wire refusal and strand the launch with no fallback. worktree_creation was a consequence of createWorkerWorktree creating agent-first: its startup terminal WAS the worker, so the structured branch below it was unreachable for any new worktree. A structured worker now creates the worktree with no startup agent and creates its session for the worktree afterwards — the order the renderer's own structured worktree create already uses. Because the executing host can only answer agentSession.createSupport for a workspace that exists, that verdict moved after creation: a refusal (WSL, and the rest) becomes a terminal agent in the worktree just created, never a failed start. --on and --terminal still downgrade, with their reasons intact, and every remaining downgrade still states itself in the mode receipt. The wait-for-setup gate is preserved explicitly. A PTY worker got it for free — agent-first creation sequences the agent's startup command behind the setup runner, so tui-idle could not arrive until setup exited. A structured session has no startup command to sequence, so the gate is now awaited directly, bounded by the start's own timeout. Split out worker-worktree-creation.ts and worker-start-agent-placement.ts rather than growing two files that were both pinned at the max-lines cap. * refactor(native-chat): make shared feasibility authoritative for launch routing * Type the structured setup gate's absent blocked reason so the wait union stays property-typed The type-aware audit rejected the blocked-reason template literal: narrowing the wait union with an 'in' check left the field typed unknown. Declaring that a structured setup gate never carries a blocked reason restores the direct read. --------- Co-authored-by: Merge Sim <sim@local>
126 lines
4.7 KiB
TypeScript
126 lines
4.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import type * as SharedLaunchRoute from '../../src/shared/structured-native-chat-launch-route'
|
|
import { decideWorkerStartMode } from '../../src/main/runtime/rpc/methods/orchestration-worker-start-mode'
|
|
import {
|
|
resolveAgentLaunchRoute,
|
|
structuredAgentLaunchSupported,
|
|
type AgentLaunchRoutingInput
|
|
} from '../../src/renderer/src/lib/agent-launch-routing'
|
|
import { RUNTIME_CAPABILITIES } from '../../src/shared/protocol-version'
|
|
import {
|
|
resolveStructuredNativeChatSupport,
|
|
type StructuredNativeChatBlocker
|
|
} from '../../src/shared/structured-native-chat-launch-route'
|
|
|
|
vi.mock('../../src/shared/structured-native-chat-launch-route', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof SharedLaunchRoute>()
|
|
return {
|
|
...actual,
|
|
resolveStructuredNativeChatSupport: vi.fn(actual.resolveStructuredNativeChatSupport)
|
|
}
|
|
})
|
|
|
|
const settings = {
|
|
experimentalNativeChat: true,
|
|
experimentalStructuredNativeChat: true,
|
|
openAgentTabsInChatByDefault: true
|
|
}
|
|
const predicate = vi.mocked(resolveStructuredNativeChatSupport)
|
|
afterEach(() => predicate.mockReset())
|
|
|
|
const placements = [
|
|
{},
|
|
{ on: 'server-1' },
|
|
{ on: 'local' },
|
|
{ terminal: 'term_1' },
|
|
{ worktree: 'current' },
|
|
{ worktree: 'new-child' },
|
|
{ worktree: 'new-top-level' },
|
|
{ model: 'opus', effort: 'high' },
|
|
{ worktree: 'new-child', model: 'opus', effort: 'high' }
|
|
]
|
|
const blockers: StructuredNativeChatBlocker[] = [
|
|
'reused-terminal',
|
|
'agent-without-structured-session',
|
|
'draft-prompt',
|
|
'floating-workspace',
|
|
'tui-launch-customization',
|
|
'remote-execution-host',
|
|
'project-runtime',
|
|
'runtime-capability',
|
|
'runtime-capability-unknown'
|
|
]
|
|
|
|
describe('shared feasibility owns every caller decision', () => {
|
|
it.each(placements)('orchestration cannot override the shared verdict for %j', (placement) => {
|
|
for (const agent of ['claude', 'codex', 'grok', 'openclaude'] as const) {
|
|
for (const customized of [false, true]) {
|
|
const input = {
|
|
params: { agent, ...placement },
|
|
settings: {
|
|
...settings,
|
|
...(customized ? { agentDefaultArgs: { [agent]: '--custom' } } : {})
|
|
}
|
|
}
|
|
predicate.mockReturnValue({ supported: true })
|
|
expect(decideWorkerStartMode(input).mode).toBe('structured')
|
|
expect(predicate).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
agent,
|
|
executionHostId: placement.on ? `runtime:${placement.on}` : 'local',
|
|
reusesTerminal: Boolean(placement.terminal),
|
|
requiresTuiLaunchCustomization: customized
|
|
})
|
|
)
|
|
for (const blocker of blockers) {
|
|
predicate.mockReturnValue({ supported: false, blocker })
|
|
const receipt = decideWorkerStartMode(input)
|
|
expect(receipt).toMatchObject({ mode: 'terminal', preferred: 'structured' })
|
|
expect(receipt.reason).not.toBe('user_default')
|
|
expect(receipt.detail).toContain('Your default is a structured chat session')
|
|
if (blocker === 'runtime-capability-unknown') {
|
|
expect(receipt.reason).toBe('structured_support_unknown')
|
|
expect(receipt.detail).toContain('has not established')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it('renderer presentation cannot override shared feasibility', () => {
|
|
for (const agent of ['claude', 'codex', 'grok', 'openclaude'] as const) {
|
|
for (const executionHostId of ['local', 'ssh:host-1']) {
|
|
for (const promptDelivery of ['auto-submit', 'draft'] as const) {
|
|
const input: AgentLaunchRoutingInput = {
|
|
settings,
|
|
agent,
|
|
executionHostId,
|
|
promptDelivery,
|
|
hostCapabilities: RUNTIME_CAPABILITIES,
|
|
requiresTuiLaunchCustomization: true,
|
|
workspaceKind: 'folder',
|
|
initialSessionOptions: { model: 'model-1', effort: 'high' }
|
|
}
|
|
predicate.mockReturnValue({ supported: true })
|
|
expect(resolveAgentLaunchRoute(input)).toBe('structured-native-chat')
|
|
expect(structuredAgentLaunchSupported(input)).toBe(true)
|
|
expect(predicate).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
agent,
|
|
executionHostId,
|
|
isDraftPrompt: promptDelivery === 'draft',
|
|
requiresTuiLaunchCustomization: true,
|
|
workspaceKind: 'folder'
|
|
})
|
|
)
|
|
for (const blocker of blockers) {
|
|
predicate.mockReturnValue({ supported: false, blocker })
|
|
expect(resolveAgentLaunchRoute(input)).not.toBe('structured-native-chat')
|
|
expect(structuredAgentLaunchSupported(input)).toBe(false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|