test(launch): pin what each agent launcher produces on main today (#27189)

* test(launch): pin the window planner's quoting platform per workspace

* test(launch): pin the runtime a window pane sends on spawn

* test(launch): pin the host's quoting platform for repos and folders

* test(launch): pin the runtime assembler's shell per workspace

* test(launch): pin that a queued tab startup does not survive a restart

* test(launch): pin the quick composer's startup handed to workspace creation

* test(launch): pin the window-lane spawn assembler's shell, env and telemetry per launcher

* test(launch): pin what host-lane launches hand the terminal spawn

* test(launch): pin the spawn request desktop automations send, local and paired

* test(launch): pin typed-prompt, transcript-continue, OpenCode and paired new-tab launches

* test(launch): pin work-item quoting, create payloads, prompted paired tabs and the WSL cwd shell rule

* test(launch): type launch fixtures instead of casting them

* test(launch): drive both launch lanes to the provider from shared per-producer tables

* test(launch): pin rows 4, 5r, 7 and 8 through real producers and shared tables; fold duplicates

* test(launch): type the parity tables and harnesses

* test(launch): pin the pane identity keys main stamps on both lanes

* test(launch): drop a redundant cast and name the automation case fields

* test(launch): build every parity row as a real producer does; pin kitty, pane size, env values, hidden and paired-host spawns

Round-2 review fixes: local Windows ConPTY panes withhold the kitty flag, the pane's
measured grid reaches the provider, agent/caller env values and the hidden mark are
provider facts, rows 5r/7 run through the host's RPC handlers, SSH folder rows quote an
apostrophe, Windows clients use their real '' default shell, titled automations only,
the quick composer drives its real selection hook, and inputs no producer builds are gone.

* test(launch): drop the automation missing-folder row, which cannot fail

* test(launch): feed the composer hook its real eligible repos; type the paired RPC request

* test(launch): quick commands auto-submit, mobile agent.launch for row 10, real desktop caller, older-host seeding, local Windows folder composer, gone automation root

* test(launch): give each AI-button row the button that reaches its workspace kind

* test(launch): re-pin to main's execution-host platform: hosts report their OS, wsl$ and SSH folders quote for Linux, older paired hosts get the host's quoting

* test(launch): automations and window tabs share the execution-host quoting rule

* test(launch): give the test-only parity modules test suffixes

* test(launch): cut the parity tables to the rows a later plan PR checks against

Drop SSH rows (the SSH PTY provider is gone from main), rows whose behaviour is
already decided to change, rows existing tests already pin, and the hostAdmittedMembership
assertion main removed. Workspace-creation and paired new-tab producer tests move to C4.
Each remaining suite names the later PR that relies on it.

* test(launch): case runs pass full settings to the shared renderer fixture

The focus-read ratchet counts getDefaultSettings in a new test fixture; the suites
build the settings instead. Type the row 8 telemetry literal.
This commit is contained in:
Brennan Benson
2026-10-11 17:00:10 -07:00
committed by GitHub
parent 8cc779e576
commit e5a4e0c96f
11 changed files with 1967 additions and 0 deletions
@@ -0,0 +1,148 @@
// Test-only: host-lane parity cases whose caller already built the launch (row 8, run verbatim),
// or sends a paired host its intent (row 7) or a model pick (row 4).
import { POSIX_PATH, WIN_PATH } from '../../shared/launch-parity-window-cases.test-fixture'
import {
AGENT_PHONE,
CMD,
CMD_EXE,
HOST_LEAF_ID,
HOST_TAB_ID,
repo,
ZSH,
type HostLaunchCall,
type HostLaunchCase,
type HostProviderFacts
} from './pty-launch-parity-host.test-cases'
// A window-built codex startup quoted for a Windows cmd.exe client; the host must not re-quote it.
const CLIENT_CODEX = 'codex "--dangerously-bypass-approvals-and-sandbox"'
const CALLER_ENV = { CUSTOM_FLAG: '1' }
const NEW_WORKSPACE = {
agent_kind: 'codex',
launch_source: 'new_workspace_composer',
request_kind: 'new'
} as const
export const OPENCODE_MODEL = 'opencode/fledge-alpha-free'
const rpc = (
method: 'terminal.create' | 'terminal.createAgentSession',
params: Record<string, unknown>
): HostLaunchCall => ({ kind: 'rpc', method, params })
const POSIX_CLAUDE = "claude '--dangerously-skip-permissions'"
const CODEX = "codex '--dangerously-bypass-approvals-and-sandbox'"
// Row 7: the terminal.createAgentSession params a paired desktop's new tab sends a current host.
// main today: command, env, launchConfig and startupCommandDelivery never reach the host; agentArgs
// is the window's default args.
const PAIRED_TAB_PATH = '/worktree'
const PAIRED_TAB = {
worktree: `id:repo-1::${PAIRED_TAB_PATH}`,
viewMode: 'terminal',
presentation: 'background'
} as const
const PAIRED_TAB_CASES: [string, Record<string, unknown>, HostProviderFacts][] = [
[
'claude draft continuation, initial cwd',
{
...PAIRED_TAB,
agent: 'claude',
prompt: "review Bob's change",
promptDelivery: 'draft',
agentArgs: '--dangerously-skip-permissions',
startupCwd: `${PAIRED_TAB_PATH}/packages/app`
},
{
command: `${POSIX_CLAUDE} --prefill 'review Bob'"'"'s change'`,
cwd: `${PAIRED_TAB_PATH}/packages/app`
}
],
[
'codex auto-submit',
{
...PAIRED_TAB,
agent: 'codex',
prompt: 'fix it',
promptDelivery: 'auto-submit',
agentArgs: '--dangerously-bypass-approvals-and-sandbox'
},
{ command: `${CODEX} 'fix it'`, startupCommandDelivery: 'shell-ready' }
]
]
export const CALLER_LAUNCH_CASES: HostLaunchCase[] = [
// C1 changes the host planner; these are its only draft (--prefill) and startupCwd-from-intent rows.
...PAIRED_TAB_CASES.map(([name, params, provider]): HostLaunchCase => ({
name: `paired new tab: ${name}`,
row: '7',
os: 'linux',
workspace: repo(PAIRED_TAB_PATH),
call: rpc('terminal.createAgentSession', params),
provider: { ...provider, ...ZSH },
// main today: a paired client's launch_source never reaches the host.
telemetry: {
agent_kind: params.agent === 'claude' ? 'claude-code' : String(params.agent),
launch_source: 'unknown',
request_kind: 'new'
},
phone: { title: 'Terminal', launchAgent: String(params.agent), isActive: false }
})),
{
// C4/C5: worktree-remote.ts spawnLocalStartupAndSetupTerminals runs the caller's command and env
// without re-quoting them, and keeps telemetry.
name: 'local-git worktrees:create startup, verbatim',
row: '8',
os: 'linux',
workspace: repo(POSIX_PATH),
call: {
kind: 'create',
options: {
command: CLIENT_CODEX,
env: CALLER_ENV,
launchConfig: {
agentCommand: CLIENT_CODEX,
agentArgs: '--dangerously-bypass-approvals-and-sandbox',
agentEnv: CALLER_ENV
},
launchAgent: 'codex',
startupCommandDelivery: 'shell-ready',
surfaceOwner: false,
telemetry: NEW_WORKSPACE
}
},
provider: {
command: CLIENT_CODEX,
startupCommandDelivery: 'shell-ready',
env: CALLER_ENV,
...ZSH
},
telemetry: NEW_WORKSPACE,
phone: { ...AGENT_PHONE, launchAgent: 'codex' }
},
// C2 keeps model validation on the host while moving the spawn to the pane; the command must survive.
...(
[
['macOS repo', 'darwin', POSIX_PATH, "opencode '--standalone'", ZSH],
['Windows C: repo', 'win32', WIN_PATH, 'opencode "--standalone"', CMD_EXE]
] as const
).map(([where, os, path, command, shell]): HostLaunchCase => ({
name: `OpenCode model pick, ${where}`,
row: '4',
os,
workspace: repo(path),
settings: CMD,
// spawnIpcPty's terminal.createAgentSession for a seeded model-pick pane.
call: rpc('terminal.createAgentSession', {
worktree: `id:repo-1::${path}`,
agent: 'opencode',
launchPreferences: { model: OPENCODE_MODEL },
startupCwd: path,
placement: { tabId: HOST_TAB_ID, leafId: HOST_LEAF_ID },
presentation: 'background',
// A local Windows ConPTY pane withholds kitty.
...(path === WIN_PATH ? {} : { terminalKittyKeyboardProtocol: true })
}),
provider: { command, ...shell },
// main today: the pane's own launch_source never reaches the host.
telemetry: { agent_kind: 'opencode', launch_source: 'unknown', request_kind: 'new' },
phone: { title: 'Terminal', launchAgent: 'opencode', isActive: false }
}))
]
@@ -0,0 +1,188 @@
// Test-only: the host-lane launch parity table (rows 3, 4, 7, 8, 10). Each case is the call a host
// producer makes and the facts main hands the provider, the phone and telemetry for it.
import type { TerminalCreateOptions } from '../runtime/runtime-terminal-contracts'
import type { LaunchWorkspace } from '../../shared/launch-parity-window-request.test-fixture'
import { POSIX_PATH } from '../../shared/launch-parity-window-cases.test-fixture'
export const HOST_TAB_ID = '33333333-3333-4333-8333-333333333333'
export const HOST_LEAF_ID = '44444444-4444-4444-8444-444444444444'
export type HostLaunchCall =
| { kind: 'create'; options: TerminalCreateOptions }
/** A paired client's RPC, run through the host's method handler; clientOperationId is added. */
| {
kind: 'rpc'
method: 'terminal.create' | 'terminal.createAgentSession'
params: Record<string, unknown>
}
export type HostProviderFacts = {
command: string
/** Default: the workspace path. */
cwd?: string
shellOverride?: string
terminalWindowsWslDistro?: string | null
startupCommandDelivery?: 'shell-ready'
/** The agent or caller env keys the case set, with their values; default none. */
env?: Record<string, string>
/** Whether main marks the PTY hidden before it has a view; default true on this lane. */
hidden?: boolean
}
export type HostLaunchCase = {
name: string
/** The launch-convergence producer row. */
row: '3' | '4' | '7' | '8' | '10'
os: NodeJS.Platform
workspace: LaunchWorkspace
settings?: Record<string, unknown>
call: HostLaunchCall
provider: HostProviderFacts
/** agent_started payload, or null when main emits none. */
telemetry: Record<string, string> | null
/** The phone's first session tab. */
phone: { title: string; launchAgent?: string; isActive: boolean }
}
// rpc/methods/agent-launch-surfaces.ts createTerminalAgent options for an AI button's reserved pane.
const agentLaunch = (extra: Record<string, unknown> = {}): HostLaunchCall => ({
kind: 'create',
options: {
startupAgent: 'claude',
tabId: HOST_TAB_ID,
leafId: HOST_LEAF_ID,
requireFreshPane: true,
launchSource: 'task_page',
viewMode: 'terminal',
surfaceOwner: false,
...extra
}
})
// worker-topology.ts createExistingWorktreeWorkerTerminal options.
const worker: HostLaunchCall = {
kind: 'create',
options: {
startupAgent: 'claude',
launchSource: 'orchestration',
title: 'worker-task-1',
surfaceOwner: false
}
}
export const repo = (path: string): LaunchWorkspace => ({ kind: 'repo', path })
const folder = (path: string): LaunchWorkspace => ({ kind: 'folder', path })
// The same factory for a mobile quick command (agent.launchReplay with a reserved pane and a
// prompt): a paired phone never moves the host window.
const mobileQuickCommand = (startupPrompt: string): HostLaunchCall => ({
kind: 'create',
options: {
startupAgent: 'codex',
startupPrompt,
launchSource: 'quick_command',
tabId: HOST_TAB_ID,
leafId: HOST_LEAF_ID,
requireFreshPane: true,
viewMode: 'terminal',
surfaceOwner: false
}
})
const TASK_PAGE = { agent_kind: 'claude-code', launch_source: 'task_page', request_kind: 'new' }
// The AI button that reaches each workspace kind: fix checks needs a repo with a PR (task_page);
// in a folder, review notes' "New agent" (also on browser and markdown annotations, notes_send).
const aiButtonSource = (workspace: LaunchWorkspace): string =>
workspace.kind === 'repo' ? 'task_page' : 'notes_send'
const QUICK_COMMAND = { agent_kind: 'codex', launch_source: 'quick_command', request_kind: 'new' }
export const AGENT_PHONE = { title: 'Terminal', launchAgent: 'claude', isActive: true }
const POSIX_CLAUDE = "claude '--dangerously-skip-permissions'"
// main today: a local POSIX host hands the provider the settings shell untrimmed.
export const ZSH = { shellOverride: ' /bin/zsh ', terminalWindowsWslDistro: null }
export const CMD_EXE = { shellOverride: 'cmd.exe', terminalWindowsWslDistro: null }
export const CMD = { terminalWindowsShell: 'cmd.exe' }
// A saved default argument with an apostrophe: the byte that tells POSIX and PowerShell quoting apart.
const BOB_ARGS = {
agentDefaultArgs: { claude: `--dangerously-skip-permissions --name "Bob's box"` }
}
const BOB_POSIX = `${POSIX_CLAUDE} '--name' 'Bob'"'"'s box'`
const CODEX = "codex '--dangerously-bypass-approvals-and-sandbox'"
/** Row 3: agent.launch with no prompt; `command` + shell are what the provider gets. */
function aiButton(
name: string,
os: NodeJS.Platform,
workspace: LaunchWorkspace,
provider: HostProviderFacts,
settings?: Record<string, unknown>
): HostLaunchCase {
return {
name,
row: '3',
os,
workspace,
...(settings ? { settings } : {}),
call: agentLaunch({ launchSource: aiButtonSource(workspace) }),
provider,
telemetry: { ...TASK_PAGE, launch_source: aiButtonSource(workspace) },
phone: AGENT_PHONE
}
}
export const HOST_LAUNCH_CASES: HostLaunchCase[] = [
// C1 (host planner inputs) and C2 (AI buttons onto the pane trigger) must keep these commands.
aiButton('macOS repo', 'darwin', repo(POSIX_PATH), { command: POSIX_CLAUDE, ...ZSH }),
aiButton(
'macOS folder, saved default args',
'darwin',
folder(POSIX_PATH),
{ command: BOB_POSIX, ...ZSH },
BOB_ARGS
),
{
// C1-C7 keep the host path's prompt carry.
name: 'mobile quick command, macOS repo, codex prompt waits for the shell',
row: '10',
os: 'darwin',
workspace: repo(POSIX_PATH),
call: mobileQuickCommand("fix Bob's branch"),
provider: {
command: `${CODEX} 'fix Bob'"'"'s branch'`,
startupCommandDelivery: 'shell-ready',
...ZSH
},
telemetry: QUICK_COMMAND,
phone: { ...AGENT_PHONE, launchAgent: 'codex' }
},
// C5d/C5e (options, presentation) check host callers against these.
...[repo(POSIX_PATH), folder(POSIX_PATH)].map((workspace): HostLaunchCase => ({
name: `orchestration worker, Linux ${workspace.kind}`,
row: '10',
os: 'linux',
workspace,
call: worker,
provider: { command: POSIX_CLAUDE, ...ZSH },
telemetry: { ...TASK_PAGE, launch_source: 'orchestration' },
phone: { title: 'worker-task-1', launchAgent: 'claude', isActive: true }
}))
]
const HOST_ENV_KEYS = [
'ORCA_PANE_KEY',
'ORCA_TAB_ID',
'ORCA_WORKTREE_ID',
'ORCA_AGENT_LAUNCH_TOKEN',
'ORCA_TERMINAL_HANDLE'
]
const FOLDER_ENV_KEYS = ['ORCA_WORKSPACE_ID', 'ORCA_PROJECT_GROUP_ID', 'ORCA_WORKSPACE_ROOT']
/** What main hands the provider for a host-lane case. */
export function expectedHostProvider(c: HostLaunchCase): Record<string, unknown> {
return {
cwd: c.workspace.path.replaceAll('\\', '/'),
cols: 120,
rows: 40,
commandDelivery: 'provider',
hidden: true,
env: {},
...c.provider,
// main today: the host lane stamps ORCA_WORKSPACE_ID on folders only.
orcaEnv: c.workspace.kind === 'folder' ? [...FOLDER_ENV_KEYS, ...HOST_ENV_KEYS] : HOST_ENV_KEYS
}
}
@@ -0,0 +1,255 @@
// Test-only: one real OrcaRuntimeService wired into the real pty handlers, so both launch lanes
// reach the same fake provider. Window lane: pty:spawn. Host lane: createTerminal / createAgentSession
// through the runtime controller registerPtyHandlers installs. Suites still declare the IPC vi.mock block.
import { vi } from 'vitest'
import type { Mock } from 'vitest'
import { registerPtyHandlers } from './pty'
import { existsSyncMock, statSyncMock } from './pty-ipc-mock-registry'
import type { PtyIpcSuiteFixtures } from './pty-ipc-test-harness'
import { OrcaRuntimeService } from '../runtime/orca-runtime'
import type { TerminalWorkspaceLaunchScope } from '../runtime/runtime-legacy-worker-terminal-recovery-types'
import {
IDENTITY_ENV_KEYS,
type LaunchWorkspace
} from '../../shared/launch-parity-window-request.test-fixture'
import { isHiddenRendererPty } from './pty-hidden-delivery-gate'
import { HOST_TAB_ID } from './pty-launch-parity-host.test-cases'
const WINDOWS_ENV = {
COMSPEC: 'C:\\Windows\\system32\\cmd.exe',
USERPROFILE: 'C:\\Users\\test',
SystemRoot: 'C:\\Windows',
ProgramW6432: 'C:\\Program Files',
ProgramFiles: 'C:\\Program Files',
'ProgramFiles(x86)': undefined
}
const ORIGINAL_PLATFORM = process.platform
const savedEnv = new Map<string, string | undefined>()
let remoteSpawns = 0
/** Main's process.platform for a case; Windows also gets the env and .exe probes its shells read. */
export function setMainPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
if (platform !== 'win32') {
return
}
for (const [key, value] of Object.entries(WINDOWS_ENV)) {
if (!savedEnv.has(key)) {
savedEnv.set(key, process.env[key])
}
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
statSyncMock.mockImplementation((target: string) => {
const isExe = /\.exe$/i.test(String(target))
return { isDirectory: () => !isExe, isFile: () => isExe, size: 1024, mode: 0o755 }
})
existsSyncMock.mockReturnValue(true)
}
export function restoreMainPlatform(): void {
Object.defineProperty(process, 'platform', { configurable: true, value: ORIGINAL_PLATFORM })
for (const [key, value] of savedEnv) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
savedEnv.clear()
}
/** Settings both lanes read on main's current OS; the untrimmed default shell shows which reads trim it. */
function parityMainSettings(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
// Windows hides the default-shell setting and defaults it to ''.
terminalDefaultShell: process.platform === 'win32' ? '' : ' /bin/zsh ',
terminalDefaultShellArgs: ['-l'],
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'auto',
disabledTuiAgents: [],
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {},
// Why off: a fresh agent launch would otherwise write workspace trust into the agent's home.
agentWorkspaceTrustEnabled: false,
...overrides
}
}
function launchScope(workspace: LaunchWorkspace): TerminalWorkspaceLaunchScope {
const connectionId = null
if (workspace.kind === 'repo') {
return {
id: `repo-1::${workspace.path}`,
path: workspace.path,
connectionId,
repo: {
id: 'repo-1',
path: workspace.path,
displayName: 'repo',
badgeColor: '#000000',
addedAt: 0,
connectionId
},
folderWorkspace: null
}
}
return {
id: 'folder:fw-1',
path: workspace.path,
connectionId,
repo: null,
folderWorkspace: {
id: 'fw-1',
projectGroupId: 'pg-1',
name: 'folder',
folderPath: workspace.path,
connectionId,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
}
}
}
type ScopeResolver = {
resolveTerminalWorkspaceLaunchScope: (selector: string) => Promise<TerminalWorkspaceLaunchScope>
}
export type ParityLanes = {
runtime: OrcaRuntimeService
/** The local daemon provider's spawn mock. */
provider: Mock
persistPtyBinding: Mock
reveal: Mock
spawnWindow: (request: Record<string, unknown>) => Promise<unknown>
}
/** Wires one runtime + handlers for a workspace (its saved project runtime included). */
export function startParityLanes(
suite: Pick<PtyIpcSuiteFixtures, 'handlers' | 'mainWindow' | 'installDaemonTestProvider'>,
args: {
workspace: LaunchWorkspace
settings?: Record<string, unknown>
spawnError?: Error
}
): ParityLanes {
const settings = parityMainSettings(args.settings)
const persistPtyBinding = vi.fn(async () => true)
const { repo, folderWorkspace } = launchScope(args.workspace)
const store = {
getSettings: () => settings,
getProjects: () => [
{
id: 'project-1',
sourceRepoIds: ['repo-1'],
...(args.workspace.projectRuntime
? { localWindowsRuntimePreference: args.workspace.projectRuntime }
: {})
}
],
getRepo: (id: string) => (repo && id === repo.id ? repo : undefined),
getRepos: () => (repo ? [repo] : []),
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
getWorkspaceSession: () => ({ tabsByWorktree: {}, terminalLayoutsByTabId: {} }),
getFolderWorkspace: () => folderWorkspace ?? undefined,
getFolderWorkspaces: () => (folderWorkspace ? [folderWorkspace] : []),
getProjectGroups: () => [{ id: 'pg-1', name: 'group', parentPath: null, connectionId: null }],
persistPtyBinding
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: launches read only the store members defined above; the rest of the persistence store is unreachable here.
const runtime = new OrcaRuntimeService(store as never)
const provider = vi.fn(async (options: { sessionId?: string }) => {
if (args.spawnError) {
throw args.spawnError
}
// A fresh id per spawn, so one case's hidden mark never reads into the next.
return { id: options.sessionId ?? `remote-pty-${++remoteSpawns}` }
})
suite.installDaemonTestProvider({ spawn: provider })
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handlers read the window, runtime, settings and store members these fakes define.
const handlerArgs = [
suite.mainWindow,
runtime,
undefined,
() => settings,
undefined,
store
] as unknown as Parameters<typeof registerPtyHandlers>
registerPtyHandlers(...handlerArgs)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver is a real (protected) runtime method; the scope is this case's input, not an assertion.
const resolver = runtime as unknown as ScopeResolver
vi.spyOn(resolver, 'resolveTerminalWorkspaceLaunchScope').mockResolvedValue(
launchScope(args.workspace)
)
const reveal = vi.fn().mockResolvedValue({ tabId: HOST_TAB_ID })
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
revealTerminalSession: reveal,
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
return {
runtime,
provider,
persistPtyBinding,
reveal,
spawnWindow: async (request) => suite.handlers.get('pty:spawn')!(null, request)
}
}
const PROVIDER_FACT_KEYS = [
'command',
'cwd',
'cols',
'rows',
'shellOverride',
'terminalShellArgs',
'terminalWindowsWslDistro',
'commandDelivery',
'startupCommandDelivery'
] as const
// The agent and caller env keys the tables save; their values must reach the provider.
const CASE_ENV_KEYS = ['A', 'CUSTOM_FLAG']
/**
* The launch facts the plan names at the provider boundary: `orcaEnv` lists identity keys present,
* `env` the case's own env values, `hidden` whether main marked the PTY hidden before a view.
*/
export async function providerFacts(provider: Mock): Promise<Record<string, unknown>> {
const options = recordOf(provider.mock.calls.at(-1)?.[0])
const env = recordOf(options.env)
const spawned = recordOf(await provider.mock.results.at(-1)?.value)
return {
...Object.fromEntries(
PROVIDER_FACT_KEYS.filter((k) => k in options).map((k) => [k, options[k]])
),
orcaEnv: IDENTITY_ENV_KEYS.filter((key) => key in env),
env: Object.fromEntries(CASE_ENV_KEYS.filter((key) => key in env).map((k) => [k, env[k]])),
hidden: isHiddenRendererPty(String(spawned.id))
}
}
export function recordOf(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' ? { ...value } : {}
}
+337
View File
@@ -0,0 +1,337 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
import { classifyErrorMock, trackMock } from './pty-ipc-mock-registry'
import {
providerFacts,
recordOf,
restoreMainPlatform,
setMainPlatform,
startParityLanes,
type ParityLanes
} from './pty-launch-parity.test-fixture'
import {
POSIX_PATH,
WINDOW_LAUNCH_CASES
} from '../../shared/launch-parity-window-cases.test-fixture'
import {
expectedWindowProvider,
launchWorkspaceId,
windowSpawnRequest,
type LaunchWorkspace,
type WindowProviderFacts
} from '../../shared/launch-parity-window-request.test-fixture'
import {
AUTOMATION_LAUNCH_CASES,
automationSpawnRequest
} from '../../shared/launch-parity-automation-cases.test-fixture'
import {
expectedHostProvider,
HOST_LAUNCH_CASES,
type HostLaunchCase
} from './pty-launch-parity-host.test-cases'
import { CALLER_LAUNCH_CASES, OPENCODE_MODEL } from './pty-launch-parity-caller.test-cases'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import { DESKTOP_RPC_CALLER } from '../runtime/rpc/rpc-caller-identity'
import { DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES } from './desktop-renderer-runtime-capabilities'
import { TERMINAL_LIFECYCLE_METHODS } from '../runtime/rpc/methods/terminal/terminal-lifecycle-methods'
import { AGENT_SESSION_METHODS } from '../runtime/rpc/methods/agent-session'
import { applyManagedDataAccountEnvironment } from '../managed-data-accounts/launch-environment'
import { probeOpenCodeLaunchCapabilities } from '../opencode/opencode-launch-capabilities'
import {
probeOpenCodeModelAvailability,
resolveOpenCodeDirectModelExecutable
} from '../opencode/opencode-model-availability'
import { probeOpenCodeLaunchModelContext } from '../opencode/opencode-launch-model-context'
vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock()))
vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock()))
vi.mock('node:child_process', async (importOriginal) =>
(await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal())
)
vi.mock('../opencode/hook-service', () =>
import('./pty-ipc-mock-registry').then((m) => m.openCodeHookServiceModuleMock())
)
vi.mock('../mimo/hook-service', () =>
import('./pty-ipc-mock-registry').then((m) => m.mimoHookServiceModuleMock())
)
vi.mock('../agent-hooks/server', () =>
import('./pty-ipc-mock-registry').then((m) => m.agentHookServerModuleMock())
)
vi.mock('../pi/titlebar-extension-service', () =>
import('./pty-ipc-mock-registry').then((m) => m.piTitlebarExtensionModuleMock())
)
vi.mock('../pwsh', () => import('./pty-ipc-mock-registry').then((m) => m.pwshModuleMock()))
vi.mock('../wsl', async (importOriginal) =>
(await import('./pty-ipc-mock-registry')).wslModuleMock(await importOriginal())
)
vi.mock('../telemetry/client', () =>
import('./pty-ipc-mock-registry').then((m) => m.telemetryClientModuleMock())
)
vi.mock('../telemetry/classify-error', () =>
import('./pty-ipc-mock-registry').then((m) => m.classifyErrorModuleMock())
)
vi.mock('../cli/linux-terminal-orca-cli-shim', () =>
import('./pty-ipc-mock-registry').then((m) => m.linuxCliShimModuleMock())
)
vi.mock('../memory/pty-registry', () =>
import('./pty-ipc-mock-registry').then((m) => m.ptyRegistryModuleMock())
)
vi.mock('../agent-hooks/migration-unsupported-pty-state', () =>
import('./pty-ipc-mock-registry').then((m) => m.migrationUnsupportedPtyModuleMock())
)
vi.mock('../codex/codex-pane-account-registry', () =>
import('./pty-ipc-mock-registry').then((m) => m.codexPaneAccountRegistryModuleMock())
)
// Why: an OpenCode v2 install with the picked model available; the probes would run the binary.
vi.mock('../managed-data-accounts/launch-environment', () => ({
applyManagedDataAccountEnvironment: vi.fn()
}))
vi.mock('../opencode/opencode-launch-capabilities', () => ({
probeOpenCodeLaunchCapabilities: vi.fn()
}))
vi.mock('../opencode/opencode-model-availability', () => ({
resolveOpenCodeDirectModelExecutable: vi.fn(),
probeOpenCodeModelAvailability: vi.fn()
}))
vi.mock('../opencode/opencode-launch-model-context', () => ({
probeOpenCodeLaunchModelContext: vi.fn()
}))
// Why: every folder in the table exists on its host; main checks that before it spawns.
vi.mock('../project-groups/folder-workspace-path-status', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
getFolderWorkspacePathStatus: vi.fn(async () => ({ path: '', exists: true }))
}))
// Why: no Antigravity account is selected on this machine, so the account switch before an agy launch is a no-op.
vi.mock('../antigravity/native-account-launch', () => ({
prepareAntigravityAccountForLaunch: vi.fn(async () => {})
}))
vi.mock('../codex/codex-state-db-backfill-recovery', () =>
import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock())
)
type WindowLaneCase = {
label: string
client: NodeJS.Platform
workspace: LaunchWorkspace
settings?: Record<string, unknown>
request: Record<string, unknown>
provider: WindowProviderFacts
/** The agent env the case saved, as the provider must receive it. */
env: Record<string, string>
}
const WINDOW_LANE_CASES: WindowLaneCase[] = [
...WINDOW_LAUNCH_CASES.map((c) => ({
...c,
label: `row ${c.row}: ${c.name}`,
request: windowSpawnRequest(c),
env: c.request.agentEnv ?? {}
})),
...AUTOMATION_LAUNCH_CASES.map((c) => ({
...c,
label: `row 5: ${c.name}`,
request: automationSpawnRequest(c),
env: {}
}))
]
// Pins main's current launch behaviour as the convergence parity baseline (rows 2, 5 and 6, main
// half): each table request, fed to pty:spawn, reaches the provider with these facts (the pane's
// measured size, never hidden), one agent_started, and a pane binding.
// C5a-e and C6 check the merged spawn pipeline against it.
describe('window lane: pty:spawn request -> provider', () => {
const suite = setupPtyIpcSuite()
afterEach(() => restoreMainPlatform())
it.each(WINDOW_LANE_CASES)(
'$label',
async ({ client, workspace, settings, request, provider, env: agentEnv }) => {
// A desktop client's main process runs on the client OS.
setMainPlatform(client)
const lanes = startParityLanes(suite, { workspace, settings })
await lanes.spawnWindow(request)
expect(await providerFacts(lanes.provider)).toEqual(
expectedWindowProvider(request, provider, agentEnv)
)
expect(trackMock.mock.calls).toEqual([['agent_started', request.telemetry]])
expect(lanes.persistPtyBinding.mock.calls).toEqual([
[
{
worktreeId: request.worktreeId,
tabId: request.tabId,
leafId: request.leafId,
ptyId: expect.any(String),
startupCwd: expect.any(String),
...pick(request, ['placement']),
origin: 'spawn'
}
]
])
}
)
})
function pick(source: Record<string, unknown>, keys: string[]): Record<string, unknown> {
return Object.fromEntries(keys.filter((key) => key in source).map((key) => [key, source[key]]))
}
function stubOpenCodeV2(): void {
vi.mocked(applyManagedDataAccountEnvironment).mockReset()
vi.mocked(resolveOpenCodeDirectModelExecutable).mockReset().mockResolvedValue('/bin/opencode')
vi.mocked(probeOpenCodeLaunchCapabilities)
.mockReset()
.mockResolvedValue({ version: '2.0.16', pluginApi: 'v2', promptMode: 'prefill' })
vi.mocked(probeOpenCodeModelAvailability).mockReset().mockResolvedValue(true)
vi.mocked(probeOpenCodeLaunchModelContext)
.mockReset()
.mockResolvedValueOnce({
primaryAgent: 'build',
availableModels: [OPENCODE_MODEL],
primaryModel: 'opencode/big-pickle'
})
.mockResolvedValue({
primaryAgent: 'build',
availableModels: [OPENCODE_MODEL],
primaryModel: OPENCODE_MODEL
})
}
async function runHostCall(lanes: ParityLanes, c: HostLaunchCase): Promise<unknown> {
if (c.call.kind === 'create') {
return lanes.runtime.createTerminal(`id:${launchWorkspaceId(c.workspace)}`, c.call.options)
}
const { method, params } = c.call
const dispatcher = new RpcDispatcher({
runtime: lanes.runtime,
methods: [...TERMINAL_LIFECYCLE_METHODS, ...AGENT_SESSION_METHODS]
})
const response = await dispatcher.dispatch(
{
id: 'parity',
// The transport authenticated the caller before dispatch; the dispatcher only carries it.
authToken: 'token',
method,
params:
method === 'terminal.createAgentSession'
? { clientOperationId: `${Date.now()}-0123456789abcdef0123456789abcdef`, ...params }
: params
},
// Row 4 is the desktop's own runtime:call (ipc/runtime.ts); row 7 comes from a paired desktop.
c.row === '4'
? {
clientId: 'desktop-renderer',
caller: DESKTOP_RPC_CALLER,
clientKind: 'runtime',
connectionId: 'desktop-window-1',
clientCapabilities: DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES
}
: { clientId: 'device-1', pairedDeviceId: 'device-1', clientKind: 'runtime' }
)
if (!response.ok) {
throw Object.assign(new Error(response.error.message), { code: response.error.code })
}
return response.result
}
// Pins main's current launch behaviour as the convergence parity baseline (rows 3, 4, 7, 8, 10):
// each host producer's call reaches the provider through the real runtime controller with these
// facts, telemetry, placement and phone tab. C1, C2 and C5a-e check the host path against it.
describe('host lane: producer call -> runtime controller -> provider', () => {
const suite = setupPtyIpcSuite()
beforeEach(() => stubOpenCodeV2())
afterEach(() => restoreMainPlatform())
it.each([...HOST_LAUNCH_CASES, ...CALLER_LAUNCH_CASES])('row $row: $name', async (c) => {
setMainPlatform(c.os)
const lanes = startParityLanes(suite, { workspace: c.workspace, settings: c.settings })
await runHostCall(lanes, c)
expect(await providerFacts(lanes.provider)).toEqual(expectedHostProvider(c))
// The host stamps its own pane identity (the window lane's stale-key case is below).
const spawned = recordOf(lanes.provider.mock.calls.at(-1)?.[0])
expect(recordOf(spawned.env).ORCA_PANE_KEY).toBe(spawned.paneKey)
expect(trackMock.mock.calls).toEqual(c.telemetry ? [['agent_started', c.telemetry]] : [])
expect(lanes.persistPtyBinding.mock.calls[0]?.[0]).toMatchObject({
placement: { kind: 'new-tab' }
})
const phone = await lanes.runtime.listMobileSessionTabs(`id:${launchWorkspaceId(c.workspace)}`)
expect(pick({ ...phone.tabs[0] }, ['title', 'launchAgent', 'isActive'])).toEqual(c.phone)
})
})
const POSIX_REPO = { kind: 'repo', path: POSIX_PATH } as const
const WORKER = HOST_LAUNCH_CASES.find((c) => c.name.startsWith('orchestration worker'))!
// Pins main's current launch behaviour as the convergence parity baseline (rows 2 and 10; failed
// spawn, reveal, pane identity): the facts no single table row carries.
describe('launch facts outside the tables', () => {
const suite = setupPtyIpcSuite()
afterEach(() => restoreMainPlatform())
// C5a (one agent_error place): main today reports a failed window-lane spawn.
it.each([
{
lane: 'telemetry kind wins over the claude sniff',
request: { command: 'claude', telemetry: { agent_kind: 'opencode' } },
calls: [['agent_error', { agent_kind: 'opencode', error_class: 'binary_not_found' }]]
},
{
lane: 'local claude without telemetry',
request: { command: 'claude' },
calls: [['agent_error', { agent_kind: 'claude-code', error_class: 'binary_not_found' }]]
}
])('a failed window spawn, $lane, emits agent_error', async (c) => {
setMainPlatform('darwin')
classifyErrorMock.mockReturnValue({ error_class: 'binary_not_found' })
const spawnError = new Error('spawn boom')
const lanes = startParityLanes(suite, { workspace: POSIX_REPO, spawnError })
const launch = lanes.spawnWindow({
cols: 80,
rows: 24,
cwd: POSIX_PATH,
worktreeId: launchWorkspaceId(POSIX_REPO),
...c.request
})
await expect(launch).rejects.toThrow('spawn boom')
expect(trackMock.mock.calls.filter(([event]) => event === 'agent_error')).toEqual(c.calls)
})
// C5e (presentation merge): a worker asks the window to reveal its titled tab without activating it.
it('row 10: an orchestration worker requests a titled, inactive reveal', async () => {
setMainPlatform(WORKER.os)
const lanes = startParityLanes(suite, { workspace: WORKER.workspace })
await runHostCall(lanes, WORKER)
expect(lanes.reveal.mock.calls[0]?.[1]).toMatchObject({
title: 'worker-task-1',
activate: false,
surfaceOwner: false
})
expect(lanes.reveal.mock.calls[0]?.[1]).not.toHaveProperty('presentation')
})
// C5c (pane identity built once in main): main rewrites stale pane keys from the request's tab and
// leaf; workspace keys pass.
it('window lane: the provider gets the pane keys main derives, not the stale ones sent', async () => {
setMainPlatform('darwin')
const tabId = '55555555-5555-4555-8555-555555555555'
const leafId = '66666666-6666-4666-8666-666666666666'
const lanes = startParityLanes(suite, { workspace: POSIX_REPO })
await lanes.spawnWindow({
cols: 80,
rows: 24,
cwd: POSIX_PATH,
worktreeId: launchWorkspaceId(POSIX_REPO),
tabId,
leafId,
env: { ORCA_WORKSPACE_ID: 'folder:f1', ORCA_PANE_KEY: 'stale', ORCA_TAB_ID: 'stale' }
})
const env = recordOf(recordOf(lanes.provider.mock.calls.at(-1)?.[0]).env)
expect(pick(env, ['ORCA_WORKSPACE_ID', 'ORCA_PANE_KEY', 'ORCA_TAB_ID'])).toEqual({
ORCA_WORKSPACE_ID: 'folder:f1',
ORCA_PANE_KEY: `${tabId}:${leafId}`,
ORCA_TAB_ID: tabId
})
})
})
@@ -0,0 +1,196 @@
import type * as React from 'react'
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { flushAsyncTicks } from './pty-connection-test-async'
import { createManager, createPane } from './pty-connection-test-pane-fixtures'
import { buildPaneConnectionDeps } from './pty-connection-test-deps'
import {
installTerminalTestGlobals,
restoreTerminalTestGlobals
} from './pty-connection-test-environment'
import {
launchWorkspaceState,
perClientLoader,
type LaunchStoreHolder
} from '@/lib/launch-parity-renderer.test-fixture'
import { getDefaultSettings } from '../../../../shared/constants'
import { WINDOW_LAUNCH_CASES } from '../../../../shared/launch-parity-window-cases.test-fixture'
import {
LAUNCH_TAB_ID,
LAUNCH_TOKEN,
PANE_GRID,
launchWorkspaceId,
windowSpawnRequest,
type WindowLaunchCase
} from '../../../../shared/launch-parity-window-request.test-fixture'
const holder = vi.hoisted((): LaunchStoreHolder => ({ store: null }))
vi.mock('@/store', async () =>
(await import('@/lib/launch-parity-renderer.test-fixture')).launchStoreModuleMock(holder)
)
vi.mock('@/runtime/sync-runtime-graph', () => ({ scheduleRuntimeGraphSync: vi.fn() }))
vi.mock('@/lib/agent-status', async (importOriginal) => {
const { buildAgentStatusModuleMock } = await import('./pty-connection-test-environment')
return buildAgentStatusModuleMock(await importOriginal<Record<string, unknown>>())
})
vi.mock('./cache-timer-seeding', () => ({ shouldSeedCacheTimerOnInitialTitle: () => false }))
vi.mock('sonner', () => ({ toast: { info: vi.fn(), message: vi.fn(), error: vi.fn() } }))
vi.mock('@/lib/codex-stale-pane-sweep', () => ({ notifyCodexPaneBoundForStaleSweep: vi.fn() }))
vi.mock('react', async (importOriginal) => {
const actual = await importOriginal<typeof React>()
return { ...actual, useCallback: <T>(fn: T): T => fn }
})
vi.mock('@/lib/windows-terminal-capabilities', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
hasCachedWindowsTerminalCapabilities: () => true,
getCachedWindowsTerminalCapabilities: () => ({
wslAvailable: true,
wslDistros: ['Ubuntu'],
pwshAvailable: false,
gitBashAvailable: false
})
}))
const load = perClientLoader(async () => ({
launch: await import('@/lib/launch-agent-in-new-tab'),
quick: await import('@/lib/run-quick-command-in-new-tab'),
vault: await import('@/lib/launch-ai-vault-session'),
resume: await import('@/lib/ai-vault-resume-command'),
pane: await import('./pty-connection'),
paneOptions: await import('./terminal-pane-manager-options'),
defaults: await import('@/lib/pane-manager/pane-terminal-options'),
lifecycle: await import('./terminal-pane-lifecycle-primitives')
}))
/** Runs the case's real producer against a real store; returns the tab it opened. */
async function openTab(c: WindowLaunchCase) {
const modules = await load(c.client)
await installTerminalTestGlobals()
Object.assign(window.api.platform, { get: () => ({ platform: c.client, osRelease: '10' }) })
const store = modules.createStore()
holder.store = store
store.setState(
launchWorkspaceState(c.workspace, { ...getDefaultSettings('/home/alice'), ...c.settings })
)
const worktreeId = launchWorkspaceId(c.workspace)
const producer = c.producer
if (producer.kind === 'quick-command') {
modules.quick.runQuickCommandInNewTab({
command: { id: 'qc-1', label: 'Fix', action: 'agent-prompt', ...producer },
worktreeId
})
} else if (producer.kind === 'continuation') {
// What launchAgentSessionContinuation passes once the agent is detected.
modules.launch.launchAgentInNewTab({
requestId: `parity-${c.name}`,
agent: producer.agent,
worktreeId,
prompt: producer.prompt,
promptDelivery: 'draft',
launchSource: 'terminal_context_menu',
initialCwd: producer.initialCwd
})
} else {
// The sidebar's "continue from transcript" builds the startup, then opens the tab.
const startup = modules.resume.buildAiVaultResumeStartupForWorktree({
state: store.getState(),
worktreeId,
session: {
agent: 'antigravity',
sessionId: 'ide-id',
codexHome: null,
executionHostId: 'local',
filePath: producer.transcript,
cwd: producer.cwd
}
})
modules.vault.launchAiVaultSessionInNewTab({ agent: 'antigravity', worktreeId, ...startup })
}
const tab = store.getState().tabsByWorktree[worktreeId]?.at(-1)
if (!tab) {
throw new Error(`${c.name}: the producer opened no tab`)
}
return { modules, store, worktreeId, tab }
}
/** Mounts the tab's pane the way TerminalPane does and returns the pty:spawn request it sends. */
async function paneSpawnRequest(c: WindowLaunchCase): Promise<unknown> {
const { modules, store, worktreeId, tab } = await openTab(c)
const spawn = vi.fn(async (_request: Record<string, unknown>) => ({ id: 'pty-1' }))
const noop = (): (() => void) => () => {}
Object.assign(window.api.pty, {
spawn,
onData: noop,
onReplay: noop,
onExit: noop,
onWriteUnavailable: noop,
onSideEffect: noop,
resize: vi.fn(),
claimViewport: vi.fn()
})
// As TerminalPane mounts: a queued initial cwd, else the tab's startup cwd, else the workspace.
const cwd = modules.lifecycle.resolveQueuedInitialCwd(
undefined,
() => store.getState().consumeTabInitialCwd(tab.id),
tab.startupCwd ?? c.workspace.path
).startupCwd
const startup = store.getState().pendingStartupByTabId[tab.id]
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the real store state includes every StoreState member the pane reads.
const deps = buildPaneConnectionDeps(() => store.getState() as never, {
tabId: tab.id,
worktreeId,
cwd,
startup
})
const pane = createPane(1)
// A measured grid no lane defaults to, so the request shows the pane's size reached it.
Object.assign(pane.terminal, PANE_GRID)
// The xterm keyboard options the real TerminalPane builds for this tab, over the defaults (pane-dom-creation.ts).
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: building the bag and terminalOptions read only these deps, ptyDeps.startup and startupCwd.
const managerOptions = modules.paneOptions.createTerminalPaneManagerOptions({
deps: {
tabId: tab.id,
worktreeId,
settingsRef: { current: store.getState().settings },
isVisibleRef: { current: true },
effectiveMacOptionAsAltRef: { current: 'false' }
},
ptyDeps: { startup },
startupCwd: cwd
} as never)
const { vtExtensions } = {
...modules.defaults.buildDefaultTerminalOptions(),
...managerOptions.terminalOptions?.(pane.id)
}
pane.terminal.options.vtExtensions = { kittyKeyboard: vtExtensions?.kittyKeyboard === true }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pane fixtures model every member connectPanePty reads, as in pty-connection-startup-command-delivery.test.ts.
modules.pane.connectPanePty(pane as never, createManager(1) as never, deps as never)
await flushAsyncTicks(20)
const [request] = spawn.mock.calls[0] ?? []
// Fixed ids so the table can name them; the color replies are this harness's theme.
const { terminalColorQueryReplies: _theme, ...rest } = request ?? {}
return JSON.parse(
JSON.stringify(rest)
.replaceAll(tab.id, LAUNCH_TAB_ID)
.replaceAll(String(rest.launchToken), LAUNCH_TOKEN)
)
}
// Pins main's current launch behaviour as the convergence parity baseline (rows 2 and 6, window
// half): the real producer, store and pane build the pty:spawn request each case's table row names.
// C2 (typed-prompt tabs onto the pane trigger) and C3 (Antigravity continue) check against it.
describe('window-tab producers: the pty:spawn request a real pane sends', () => {
// Why: the first import transforms the whole pane graph; keep that out of the first case.
beforeAll(async () => {
await load('darwin')
await load('win32')
}, 240_000)
afterEach(async () => {
await restoreTerminalTestGlobals()
vi.unstubAllGlobals()
})
it.each(WINDOW_LAUNCH_CASES)('row $row: $name', async (c) => {
expect(await paneSpawnRequest(c)).toEqual(windowSpawnRequest(c))
})
})
@@ -0,0 +1,103 @@
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import {
launchWorkspaceState,
perClientLoader,
type LaunchStoreHolder
} from '@/lib/launch-parity-renderer.test-fixture'
import { getDefaultSettings } from '../../../shared/constants'
import {
AUTOMATION_LAUNCH_CASES,
AUTOMATION_PROMPT,
automationPhoneTab,
automationSpawnRequest,
type AutomationLaunchCase
} from '../../../shared/launch-parity-automation-cases.test-fixture'
import {
LAUNCH_LEAF_ID,
LAUNCH_TAB_ID,
LAUNCH_TOKEN,
launchWorkspaceId
} from '../../../shared/launch-parity-window-request.test-fixture'
const holder = vi.hoisted((): LaunchStoreHolder => ({ store: null }))
vi.mock('@/store', async () =>
(await import('@/lib/launch-parity-renderer.test-fixture')).launchStoreModuleMock(holder)
)
// The stdin-after-start paste is pinned by launch-agent-background-session.test.ts.
vi.mock('@/lib/agent-paste-draft', () => ({ pasteDraftWhenAgentReady: vi.fn() }))
vi.mock('@/components/terminal-pane/pty-dispatcher', () => ({
registerEagerPtyBuffer: vi.fn(),
subscribeToPtyExit: vi.fn(() => () => {})
}))
vi.mock('@/components/terminal-pane/pty-data-sidecar-subscriptions', () => ({
subscribeToPtyData: vi.fn(() => () => {})
}))
const load = perClientLoader(async () => ({
launch: await import('./launch-agent-background-session'),
phone: await import('@/runtime/sync-runtime-graph/mobile-session-snapshots')
}))
/** Runs one automation in a local workspace. */
async function runAutomation(c: AutomationLaunchCase) {
const modules = await load(c.client)
const store = modules.createStore()
holder.store = store
store.setState(
launchWorkspaceState(c.workspace, { ...getDefaultSettings('/home/alice'), ...c.settings })
)
const spawn = vi.fn(async (_request: Record<string, unknown>) => ({ id: 'pty-1' }))
vi.stubGlobal('window', {
dispatchEvent: vi.fn(),
api: { pty: { spawn, write: vi.fn(), kill: vi.fn() }, runtime: { call: vi.fn() } }
})
await modules.launch.launchAgentBackgroundSession({
agent: c.agent,
worktreeId: launchWorkspaceId(c.workspace),
prompt: AUTOMATION_PROMPT,
// Automation dispatch always passes 'unknown'.
launchSource: 'unknown',
title: c.title
})
const phone = modules.phone.buildMobileSessionTabSnapshots(store.getState(), false)
return { spawn, phone }
}
/** Swaps the random tab, leaf and token ids for the table's fixed ones. */
function withFixedIds(value: unknown): unknown {
const text = JSON.stringify(value)
const ids = [
...new Set(text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g))
]
const fixed = [LAUNCH_TAB_ID, LAUNCH_LEAF_ID, LAUNCH_TOKEN]
return JSON.parse(ids.reduce((out, id, index) => out.replaceAll(id, fixed[index] ?? id), text))
}
// Pins main's current launch behaviour as the convergence parity baseline (row 5): the pty:spawn
// request a desktop automation sends and its phone tab. C7 replaces this producer.
describe('row 5: desktop automation pty:spawn request', () => {
beforeAll(async () => {
await load('darwin')
await load('linux')
await load('win32')
}, 240_000)
afterEach(() => vi.unstubAllGlobals())
it.each(AUTOMATION_LAUNCH_CASES)('$name', async (c) => {
const { spawn, phone } = await runAutomation(c)
expect(withFixedIds(spawn.mock.calls[0]?.[0])).toEqual(automationSpawnRequest(c))
expect(
phone.map(({ tabs }) =>
tabs.map((tab) => ({
title: tab.title,
launchAgent: recordOf(tab).launchAgent,
isActive: tab.isActive
}))
)
).toEqual([[automationPhoneTab(c)]])
})
})
function recordOf(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' ? { ...value } : {}
}
@@ -0,0 +1,148 @@
// Test-only harness shared by the renderer launch-parity suites: per-client module loading and a
// real store seeded with one workspace. Each suite still declares its own vi.mock calls.
import { vi } from 'vitest'
import type * as StoreHelpersNamespace from '@/store/slices/store-test-helpers'
import type * as CapabilitiesNamespace from '@/runtime/local-runtime-capabilities'
import {
LAUNCH_FOLDER_ID,
LAUNCH_REPO_ID,
launchWorkspaceId,
type LaunchClient,
type LaunchWorkspace
} from '../../../shared/launch-parity-window-request.test-fixture'
const USER_AGENT: Record<LaunchClient, string> = {
darwin: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Orca',
linux: 'Mozilla/5.0 (X11; Linux x86_64) Orca',
win32: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Orca'
}
type StoreHelpers = typeof StoreHelpersNamespace
export type LaunchTestStore = ReturnType<StoreHelpers['createTestStore']>
/** Hoist with vi.hoisted so the `@/store` mock and the suite share the live store. */
export type LaunchStoreHolder = { store: LaunchTestStore | null }
export function launchStoreModuleMock(holder: LaunchStoreHolder): Record<string, unknown> {
const live = (): LaunchTestStore => {
if (!holder.store) {
throw new Error('launch-parity store not seeded')
}
return holder.store
}
return {
useAppStore: {
getState: () => live().getState(),
setState: (...args: Parameters<LaunchTestStore['setState']>) => live().setState(...args),
subscribe: (...args: Parameters<LaunchTestStore['subscribe']>) => live().subscribe(...args)
}
}
}
/**
* Imports `load` once per client OS. Why: CLIENT_PLATFORM reads the user agent at import and
* the renderer app platform caches its first answer, so each client needs its own module graph.
*/
export function perClientLoader<T>(
load: () => Promise<T>
): (client: LaunchClient) => Promise<T & { createStore: StoreHelpers['createTestStore'] }> {
const cache = new Map<LaunchClient, T & { createStore: StoreHelpers['createTestStore'] }>()
return async (client) => {
vi.stubGlobal('navigator', { userAgent: USER_AGENT[client] })
const cached = cache.get(client)
if (cached) {
return cached
}
vi.resetModules()
const helpers: StoreHelpers = await import('@/store/slices/store-test-helpers')
const capabilities: typeof CapabilitiesNamespace =
await import('@/runtime/local-runtime-capabilities')
// The local runtime has answered (without structured support), so no launch waits on it.
capabilities.setLocalRuntimeCapabilitiesForTests([])
const loaded = { ...(await load()), createStore: helpers.createTestStore }
cache.set(client, loaded)
return loaded
}
}
/** The store state one workspace needs; `settings` is the full settings object the case runs with. */
export function launchWorkspaceState(
workspace: LaunchWorkspace,
settings: Record<string, unknown>
): Record<string, unknown> {
const repoPath = workspace.kind === 'repo' ? workspace.path : workspace.repoPath
const repo = repoPath
? {
id: LAUNCH_REPO_ID,
path: repoPath,
displayName: 'repo',
badgeColor: '#000000',
addedAt: 0,
kind: 'git',
connectionId: null,
executionHostId: 'local'
}
: null
const worktree = {
id: `${LAUNCH_REPO_ID}::${repoPath}`,
repoId: LAUNCH_REPO_ID,
path: repoPath,
hostId: 'local',
head: 'abc',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true,
displayName: 'wt',
comment: '',
linkedIssue: null,
linkedPR: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}
return {
settings,
repos: repo ? [repo] : [],
projects: repo
? [
{
id: LAUNCH_REPO_ID,
displayName: 'repo',
badgeColor: '#000000',
sourceRepoIds: [LAUNCH_REPO_ID],
createdAt: 0,
updatedAt: 0,
...(workspace.projectRuntime
? { localWindowsRuntimePreference: workspace.projectRuntime }
: {})
}
]
: [],
worktreesByRepo: repo ? { [LAUNCH_REPO_ID]: [worktree] } : {},
folderWorkspaces:
workspace.kind === 'folder'
? [
{
id: LAUNCH_FOLDER_ID.slice('folder:'.length),
projectGroupId: 'pg-1',
name: 'folder',
folderPath: workspace.path,
connectionId: null,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
}
]
: [],
projectGroups: [{ id: 'pg-1', name: 'group', parentPath: null, parentGroupId: null }],
activeWorktreeId: launchWorkspaceId(workspace)
}
}
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest'
import { parseWorkspaceSession } from '../../../../shared/workspace-session-schema'
import { buildWorkspaceSessionPayload } from '@/lib/workspace-session'
import { createTestStore, makeWorktree, seedStore } from '@/store/slices/store-test-helpers'
const WORKTREE_ID = 'repo1::/wt-1'
const STARTUP_COMMAND = "claude '--dangerously-skip-permissions'"
function seededStore() {
const store = createTestStore()
seedStore(store, {
worktreesByRepo: {
repo1: [makeWorktree({ id: WORKTREE_ID, repoId: 'repo1', path: '/wt-1' })]
}
})
return store
}
// Pins main's current launch behaviour as the convergence parity baseline (B6; window-lane new tabs,
// rows 1-2 and 6): a queued startup that never reached a pane does not survive an app restart.
// C2 (D-b) must keep a never-mounted tab reopening as a plain shell with no launch record read.
describe('queued tab startup across an app restart on main', () => {
it('persists the agent tab but not its queued startup command', () => {
const before = seededStore()
const tab = before.getState().createTab(WORKTREE_ID, undefined, undefined, {
launchAgent: 'claude'
})
before.getState().queueTabStartupCommand(tab.id, {
command: STARTUP_COMMAND,
launchConfig: {
agentCommand: STARTUP_COMMAND,
agentArgs: '--dangerously-skip-permissions',
agentEnv: {}
}
})
expect(before.getState().pendingStartupByTabId[tab.id]?.command).toBe(STARTUP_COMMAND)
const payload = buildWorkspaceSessionPayload(before.getState())
const written = JSON.stringify(payload)
expect(written).not.toContain(STARTUP_COMMAND)
expect(written).not.toContain('pendingStartup')
const parsed = parseWorkspaceSession(JSON.parse(written))
expect(parsed.ok).toBe(true)
if (!parsed.ok) {
return
}
const after = seededStore()
after.getState().hydrateWorkspaceSession(parsed.value)
// The tab comes back as the agent's tab with no PTY and nothing queued, so its pane spawns a shell.
expect(after.getState().pendingStartupByTabId).toEqual({})
expect(after.getState().tabsByWorktree[WORKTREE_ID]).toEqual([
expect.objectContaining({ id: tab.id, launchAgent: 'claude', ptyId: null })
])
})
})
@@ -0,0 +1,133 @@
// Test-only (never imported by production code): the row 5 desktop-automation parity table.
// The renderer suite asserts launchAgentBackgroundSession sends `request`; the main suite feeds the
// same request to pty:spawn. C7 (automations onto agent.launch) and C5 check against it.
import {
LAUNCH_LEAF_ID,
LAUNCH_TAB_ID,
LAUNCH_TOKEN,
launchWorkspaceId,
type LaunchClient,
type LaunchWorkspace,
type WindowProviderFacts
} from './launch-parity-window-request.test-fixture'
import { POSIX_PATH, WIN_PATH, WSL_PATH } from './launch-parity-window-cases.test-fixture'
export type AutomationLaunchCase = {
name: string
client: LaunchClient
workspace: LaunchWorkspace
settings?: Record<string, unknown>
agent: 'claude' | 'codex' | 'aider'
/** The run title; every dispatch passes "<automation name> run <n>". */
title: string
request: { command: string; agentCommand: string; shellOverride?: string }
provider: WindowProviderFacts
}
// Why an apostrophe: it is the byte that tells POSIX and PowerShell quoting apart.
export const AUTOMATION_PROMPT = "don't stop"
const SKIP = '--dangerously-skip-permissions'
const POSIX_CLAUDE = `claude '${SKIP}'`
const posix = { command: `${POSIX_CLAUDE} 'don'"'"'t stop'`, agentCommand: POSIX_CLAUDE }
const powershell = { command: `${POSIX_CLAUDE} 'don''t stop'`, agentCommand: POSIX_CLAUDE }
const CODEX = "codex '--dangerously-bypass-approvals-and-sandbox'"
const AGENT_COMMAND = {
codex: { command: `${CODEX} 'don'"'"'t stop'`, agentCommand: CODEX },
aider: { command: "aider '--yes-always'", agentCommand: "aider '--yes-always'" }
} as const
const ZSH = { shellOverride: '/bin/zsh' }
const PWSH = { shellOverride: 'powershell.exe', terminalWindowsWslDistro: null }
const WSL_EXE = { shellOverride: 'wsl.exe', terminalWindowsWslDistro: 'Ubuntu' }
const repo = (path: string): LaunchWorkspace => ({ kind: 'repo', path })
type AutomationCaseFields = Omit<AutomationLaunchCase, 'name' | 'agent' | 'title'> &
Partial<Pick<AutomationLaunchCase, 'agent'>>
const automation = (name: string, fields: AutomationCaseFields): AutomationLaunchCase => ({
name,
agent: 'claude',
title: 'Nightly audit run 3',
...fields
})
// Only a local \\wsl$ workspace sends a shellOverride. Quoting follows the execution host's OS, the
// same rule a window tab uses; the shell does not.
export const AUTOMATION_LAUNCH_CASES: AutomationLaunchCase[] = [
automation('macOS repo', {
client: 'darwin',
workspace: repo(POSIX_PATH),
request: posix,
provider: ZSH
}),
automation('Windows C: repo, default PowerShell', {
client: 'win32',
workspace: repo(WIN_PATH),
request: powershell,
provider: PWSH
}),
automation('Windows wsl$ repo, no saved runtime', {
client: 'win32',
workspace: repo(WSL_PATH),
request: { ...posix, shellOverride: 'wsl.exe' },
provider: WSL_EXE
}),
automation('Linux repo, codex', {
client: 'linux',
agent: 'codex',
workspace: repo(POSIX_PATH),
request: AGENT_COMMAND.codex,
provider: ZSH
}),
// stdin-after-start: the prompt is pasted after launch, never on argv.
automation('Linux repo, aider', {
client: 'linux',
agent: 'aider',
workspace: repo(POSIX_PATH),
request: AGENT_COMMAND.aider,
provider: ZSH
})
]
const AGENT_KIND = { claude: 'claude-code', codex: 'codex', aider: 'aider' } as const
const AGENT_ARGS = {
claude: SKIP,
codex: '--dangerously-bypass-approvals-and-sandbox',
aider: '--yes-always'
} as const
/** The full pty:spawn request launchAgentBackgroundSession sends for this case. */
export function automationSpawnRequest(c: AutomationLaunchCase): Record<string, unknown> {
const worktreeId = launchWorkspaceId(c.workspace)
return {
cols: 120,
rows: 40,
cwd: c.workspace.path,
command: c.request.command,
...(c.request.shellOverride ? { shellOverride: c.request.shellOverride } : {}),
...(c.agent === 'codex' ? { startupCommandDelivery: 'shell-ready' } : {}),
env: {
ORCA_PANE_KEY: `${LAUNCH_TAB_ID}:${LAUNCH_LEAF_ID}`,
ORCA_TAB_ID: LAUNCH_TAB_ID,
ORCA_WORKTREE_ID: worktreeId,
ORCA_AGENT_LAUNCH_TOKEN: LAUNCH_TOKEN
},
launchConfig: {
agentCommand: c.request.agentCommand,
agentArgs: AGENT_ARGS[c.agent],
agentEnv: {}
},
launchToken: LAUNCH_TOKEN,
launchAgent: c.agent,
connectionId: null,
worktreeId,
tabId: LAUNCH_TAB_ID,
leafId: LAUNCH_LEAF_ID,
placement: { kind: 'new-tab', row: { customTitle: c.title } },
telemetry: { agent_kind: AGENT_KIND[c.agent], launch_source: 'unknown', request_kind: 'new' }
}
}
/** The phone's view of a run tab: hidden under its run title, no agent named, active. */
export function automationPhoneTab(
c: Pick<AutomationLaunchCase, 'title'>
): Record<string, unknown> {
return { title: c.title, launchAgent: undefined, isActive: true }
}
@@ -0,0 +1,219 @@
// Test-only (never imported by production code): the window-lane launch parity table for rows 2
// (typed prompt into a new tab) and 6 (continue from transcript). Renderer suites assert a real
// producer + pane sends `request`; main suites feed it to pty:spawn and assert `provider`.
import type {
LaunchClient,
LaunchWorkspace,
WindowLaunchCase,
WindowLaunchProducer,
WindowProviderFacts,
WindowRequestFacts
} from './launch-parity-window-request.test-fixture'
export const POSIX_PATH = '/home/alice/repo'
export const WIN_PATH = String.raw`C:\Users\alice\repo`
export const WSL_PATH = String.raw`\\wsl$\Ubuntu\home\alice\repo`
const SKIP = '--dangerously-skip-permissions'
const POSIX_CLAUDE = `claude '${SKIP}'`
const CMD_CLAUDE = `claude "${SKIP}"`
// The auto-submitted prompt rides on argv.
const posix = { command: `${POSIX_CLAUDE} 'fix Bob'"'"'s bug'`, agentCommand: POSIX_CLAUDE }
const ps = { command: `${POSIX_CLAUDE} 'fix Bob''s bug'`, agentCommand: POSIX_CLAUDE }
const cmd = { command: `${CMD_CLAUDE} "fix Bob's bug"`, agentCommand: CMD_CLAUDE }
const BRAIN = '.gemini/antigravity-ide/brain/ide-id/transcript_full.jsonl'
const MAC_T = `/Users/alice/${BRAIN}`
const WIN_T = `C:/Users/alice/${BRAIN}`
const LINUX_T = `/home/alice/${BRAIN}`
const reference = (path: string): string =>
`Review the chat transcript at "${path}". Confirm your understanding of the previous conversation and continue from where it left off. This starts a new CLI conversation using the original transcript as a reference.`
const agy = (path: string): WindowRequestFacts => ({
command: `agy '${SKIP}' --prompt-interactive '${reference(path)}'`,
agentCommand: `agy '${SKIP}'`
})
const repo = (path: string, extra: Partial<LaunchWorkspace> = {}): LaunchWorkspace => ({
kind: 'repo',
path,
...extra
})
const folder = (path: string, extra: Partial<LaunchWorkspace> = {}): LaunchWorkspace => ({
kind: 'folder',
path,
...extra
})
const WSL_UBUNTU = { kind: 'wsl', distro: 'Ubuntu' } as const
const HOST_SENT = { projectRuntime: { kind: 'windows-host', reason: 'global-default' } } as const
const WSL_SENT = {
projectRuntime: { kind: 'wsl', reason: 'project-override', distro: 'Ubuntu' }
} as const
const IN_PWSH = { shellOverride: 'powershell.exe' }
const IN_WSL = { shellOverride: 'wsl.exe' }
// Main runs a local pane in the settings shell; Windows adds the WSL distro it derived.
const ZSH = { shellOverride: '/bin/zsh' }
const PWSH = { shellOverride: 'powershell.exe', terminalWindowsWslDistro: null }
const WSL_EXE = { shellOverride: 'wsl.exe', terminalWindowsWslDistro: 'Ubuntu' }
const CMD_EXE = { shellOverride: 'cmd.exe', terminalWindowsWslDistro: null }
const CMD = { terminalWindowsShell: 'cmd.exe' }
function launch(
row: 2 | 6,
producer: WindowLaunchProducer,
name: string,
client: LaunchClient,
workspace: LaunchWorkspace,
request: WindowRequestFacts,
provider: WindowProviderFacts,
settings?: Record<string, unknown>
): WindowLaunchCase {
return { name, row, client, workspace, producer, request, provider, settings }
}
// A quick command's prompt; the apostrophe shows which shell the command was quoted for.
const quick = { kind: 'quick-command', agent: 'claude', prompt: "fix Bob's bug" } as const
const typed = launch.bind(null, 2, quick)
const resume = (transcript: string, cwd: string | null) =>
launch.bind(null, 6, { kind: 'transcript-continue', transcript, cwd })
export const WINDOW_LAUNCH_CASES: WindowLaunchCase[] = [
// C2 (typed-prompt tabs onto the pane trigger) and C5a-e (assembler merge) check against rows 2.
typed(
'macOS repo, saved default env',
'darwin',
repo(POSIX_PATH),
{ ...posix, agentEnv: { A: '1' } },
ZSH,
{ agentDefaultEnv: { claude: { A: '1' } } }
),
typed('macOS folder', 'darwin', folder(POSIX_PATH), posix, ZSH),
// The draft rides --prefill.
launch(
2,
{
kind: 'continuation',
agent: 'claude',
prompt: "fix Bob's bug",
initialCwd: `${POSIX_PATH}/pkg`
},
'macOS repo, terminal continuation in the pane cwd',
'darwin',
repo(POSIX_PATH),
{
command: `${POSIX_CLAUDE} --prefill 'fix Bob'"'"'s bug'`,
agentCommand: POSIX_CLAUDE,
cwd: `${POSIX_PATH}/pkg`
},
ZSH
),
launch(
2,
{ ...quick, agent: 'codex' },
'macOS repo, codex auto-submit waits for the shell',
'darwin',
repo(POSIX_PATH),
{
command: `codex '--dangerously-bypass-approvals-and-sandbox' 'fix Bob'"'"'s bug'`,
agentCommand: `codex '--dangerously-bypass-approvals-and-sandbox'`
},
ZSH
),
// C1 reproduces this Windows platform/shell rule on the host planner.
typed(
'Windows C: repo, default PowerShell',
'win32',
repo(WIN_PATH),
{ ...ps, ...IN_PWSH, ...HOST_SENT },
PWSH
),
typed(
'Windows C: repo, cmd.exe',
'win32',
repo(WIN_PATH),
{ ...cmd, shellOverride: 'cmd.exe', ...HOST_SENT },
CMD_EXE,
CMD
),
// A never-saved \\wsl$ project reads as a WSL project override from its path.
typed(
'Windows wsl$ repo, no saved runtime',
'win32',
repo(WSL_PATH),
{ ...posix, ...IN_WSL, ...WSL_SENT },
WSL_EXE
),
typed(
'Windows C: repo, project WSL',
'win32',
repo(WIN_PATH, { projectRuntime: WSL_UBUNTU }),
{ ...posix, ...IN_WSL, ...WSL_SENT },
WSL_EXE
),
typed(
'Windows C: repo, global default WSL',
'win32',
repo(WIN_PATH),
{
...posix,
...IN_WSL,
projectRuntime: { kind: 'wsl', reason: 'global-default', distro: 'Ubuntu' } as const
},
WSL_EXE,
{ localWindowsRuntimeDefault: WSL_UBUNTU }
),
typed('Windows C: folder', 'win32', folder(WIN_PATH), { ...ps, ...IN_PWSH }, PWSH),
// The folder's \\wsl$ path makes its host Linux, matching the wsl.exe createTab picks from the path.
typed('Windows wsl$ folder', 'win32', folder(WSL_PATH), { ...posix, ...IN_WSL }, WSL_EXE),
// A folder borrows the runtime of a repo it contains (or equals).
typed(
'Windows folder containing a WSL-project repo',
'win32',
folder(String.raw`C:\Users\alice`, {
repoPath: WIN_PATH,
projectRuntime: WSL_UBUNTU
}),
{ ...posix, ...IN_WSL, ...WSL_SENT },
WSL_EXE
),
// C3 (Antigravity continue) checks against rows 6.
resume(MAC_T, `${POSIX_PATH}/pkg`)(
'continue, macOS repo, transcript cwd',
'darwin',
repo(POSIX_PATH),
{
...agy(MAC_T),
cwd: `${POSIX_PATH}/pkg`
},
ZSH
),
resume(MAC_T, null)(
'continue, macOS repo, no transcript cwd',
'darwin',
repo(POSIX_PATH),
agy(MAC_T),
ZSH
),
// cmd escapes the prompt's inner quotes with a caret.
resume(WIN_T, WIN_PATH)(
'continue, Windows C: repo, cmd.exe',
'win32',
repo(WIN_PATH),
{
command: `agy "${SKIP}" --prompt-interactive "${reference(WIN_T).replace(/"/g, '^"')}"`,
agentCommand: `agy "${SKIP}"`,
shellOverride: 'cmd.exe',
...HOST_SENT
},
CMD_EXE,
CMD
),
// The \\wsl$ transcript path is rewritten to its Linux spelling; the Linux cwd goes out as is.
resume(`//wsl.localhost/Ubuntu/home/alice/${BRAIN}`, POSIX_PATH)(
'continue, Windows wsl$ WSL-project repo',
'win32',
repo(WSL_PATH, {
projectRuntime: WSL_UBUNTU
}),
{ ...agy(LINUX_T), cwd: POSIX_PATH, ...IN_WSL, ...WSL_SENT },
WSL_EXE
)
]
@@ -0,0 +1,183 @@
// Test-only (never imported by production code): turns a window-lane parity case into the exact
// pty:spawn request a real pane sends, with fixed ids so renderer and main compare the same bytes.
import type { LocalWindowsRuntimePreference } from './project-execution-runtime'
export type LaunchClient = 'darwin' | 'linux' | 'win32'
export const LAUNCH_REPO_ID = 'repo-1'
export const LAUNCH_FOLDER_ID = 'folder:fw-1'
export const LAUNCH_TAB_ID = 'tab-parity'
// The pane fixture's first leaf (pty-connection-test-pane-fixtures.ts).
export const LAUNCH_LEAF_ID = '11111111-1111-4111-8111-111111111111'
export const LAUNCH_TOKEN = 'launch-token-parity'
/** The window pane's measured grid: no lane defaults to it, so it shows the pane size reached the provider. */
export const PANE_GRID = { cols: 97, rows: 31 } as const
export type LaunchWorkspace = {
kind: 'repo' | 'folder'
path: string
/** The project's saved Windows runtime; absent = never saved. */
projectRuntime?: LocalWindowsRuntimePreference
/** Folder workspaces only: a local repo at this path, to show which repos a folder borrows. */
repoPath?: string
}
export function launchWorkspaceId(workspace: LaunchWorkspace): string {
return workspace.kind === 'folder' ? LAUNCH_FOLDER_ID : `${LAUNCH_REPO_ID}::${workspace.path}`
}
export type WindowLaunchProducer =
/** An agent-prompt quick command (run-quick-command-in-new-tab.ts): Claude and Codex auto-submit. */
| { kind: 'quick-command'; agent: 'claude' | 'codex'; prompt: string }
/** A terminal's "continue in new session": Claude gets a draft in the pane's cwd. */
| { kind: 'continuation'; agent: 'claude'; prompt: string; initialCwd: string }
| { kind: 'transcript-continue'; transcript: string; cwd: string | null }
type ProjectRuntimeSent =
| { kind: 'windows-host'; reason: 'global-default' | 'project-override' }
| { kind: 'wsl'; reason: 'global-default' | 'project-override'; distro: string }
/** What the pane sends (renderer half). Unset fields take the defaults in windowSpawnRequest. */
export type WindowRequestFacts = {
command: string
agentCommand: string
shellOverride?: string
projectRuntime?: ProjectRuntimeSent
cwd?: string
/** The agent's saved default env, sent in the pane env and the launch config. */
agentEnv?: Record<string, string>
}
/** What reaches the provider for that request (main half). */
export type WindowProviderFacts = {
shellOverride: string | undefined
terminalWindowsWslDistro?: string | null
}
export type WindowLaunchCase = {
name: string
/** The launch-convergence producer row. */
row: 2 | 6
client: LaunchClient
workspace: LaunchWorkspace
settings?: Record<string, unknown>
producer: WindowLaunchProducer
request: WindowRequestFacts
provider: WindowProviderFacts
}
const AGENT_ARGS = {
claude: '--dangerously-skip-permissions',
codex: '--dangerously-bypass-approvals-and-sandbox',
aider: '--yes-always',
antigravity: '--dangerously-skip-permissions'
} as const
function sentRuntime(runtime: ProjectRuntimeSent): Record<string, unknown> {
const base = { projectId: LAUNCH_REPO_ID, reason: runtime.reason }
return {
status: 'resolved',
runtime:
runtime.kind === 'wsl'
? {
...base,
kind: 'wsl',
hostPlatform: 'wsl',
distro: runtime.distro,
cacheKey: `${LAUNCH_REPO_ID}:wsl:${runtime.distro}`
}
: {
...base,
kind: 'windows-host',
hostPlatform: 'win32',
cacheKey: `${LAUNCH_REPO_ID}:windows-host`
}
}
}
/** The full pty:spawn request a real pane sends for this case. */
export function windowSpawnRequest(c: WindowLaunchCase): Record<string, unknown> {
const { workspace, producer, request } = c
const worktreeId = launchWorkspaceId(workspace)
const agent = producer.kind === 'transcript-continue' ? 'antigravity' : producer.agent
const cwd = request.cwd ?? workspace.path
// A local Windows ConPTY pane withholds the kitty keyboard flag (terminal-keyboard-protocol.ts).
const localConpty =
c.client === 'win32' && request.shellOverride !== 'wsl.exe' && !cwd.startsWith('\\\\wsl')
return {
...PANE_GRID,
cwd,
cwdFallback: 'worktree',
env: {
...request.agentEnv,
ORCA_WORKSPACE_ID: worktreeId,
...(workspace.kind === 'folder'
? { ORCA_PROJECT_GROUP_ID: 'pg-1', ORCA_WORKSPACE_ROOT: workspace.path }
: {}),
ORCA_PANE_KEY: `${LAUNCH_TAB_ID}:${LAUNCH_LEAF_ID}`,
ORCA_TAB_ID: LAUNCH_TAB_ID,
ORCA_WORKTREE_ID: worktreeId,
ORCA_AGENT_LAUNCH_TOKEN: LAUNCH_TOKEN
},
command: request.command,
launchConfig: {
agentCommand: request.agentCommand,
agentArgs: AGENT_ARGS[agent],
agentEnv: request.agentEnv ?? {}
},
launchToken: LAUNCH_TOKEN,
launchAgent: agent,
...(agent === 'codex' ? { startupCommandDelivery: 'shell-ready' } : {}),
worktreeId,
tabId: LAUNCH_TAB_ID,
leafId: LAUNCH_LEAF_ID,
...(request.shellOverride ? { shellOverride: request.shellOverride } : {}),
...(request.projectRuntime ? { projectRuntime: sentRuntime(request.projectRuntime) } : {}),
...(localConpty ? {} : { terminalKittyKeyboardProtocol: true }),
telemetry:
producer.kind === 'transcript-continue'
? { agent_kind: 'antigravity', launch_source: 'sidebar', request_kind: 'resume' }
: {
agent_kind: agent === 'claude' ? 'claude-code' : agent,
launch_source:
producer.kind === 'quick-command' ? 'quick_command' : 'terminal_context_menu',
request_kind: 'new'
}
}
}
// The pane identity keys the plan names; agent hook and CLI keys vary by host and are not launch facts.
export const IDENTITY_ENV_KEYS = [
'ORCA_WORKSPACE_ID',
'ORCA_PROJECT_GROUP_ID',
'ORCA_WORKSPACE_ROOT',
'ORCA_PANE_KEY',
'ORCA_TAB_ID',
'ORCA_WORKTREE_ID',
'ORCA_AGENT_LAUNCH_TOKEN',
'ORCA_TERMINAL_HANDLE'
] as const
/** What main hands the provider for a window-lane pty:spawn request (rows 2, 5 and 6). */
export function expectedWindowProvider(
request: Record<string, unknown>,
provider: WindowProviderFacts,
agentEnv: Record<string, string>
): Record<string, unknown> {
// main adds the terminal handle to the pane's env.
const env = { ...(typeof request.env === 'object' ? request.env : {}), ORCA_TERMINAL_HANDLE: '' }
return {
command: request.command,
// main normalizes the cwd to forward slashes (resolveRuntimePath).
cwd: String(request.cwd).replaceAll('\\', '/'),
// The pane's measured size, and never marked hidden before its pane shows.
cols: request.cols,
rows: request.rows,
hidden: false,
env: agentEnv,
...provider,
...(request.startupCommandDelivery
? { startupCommandDelivery: request.startupCommandDelivery }
: {}),
orcaEnv: IDENTITY_ENV_KEYS.filter((key) => key in env)
}
}