From c9cf9975acdeeea241e358241e4e59c63c1dc199 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:33:02 -0700 Subject: [PATCH] fix(worktrees): preserve gated agent startup on long paths --- src/main/runtime/orca-runtime.test.ts | 53 ++++++++--- .../src/lib/worktree-activation.test.ts | 21 ++++- src/shared/setup-agent-sequencing.test.ts | 93 +++++++++++++------ src/shared/setup-agent-sequencing.ts | 22 +++-- 4 files changed, 136 insertions(+), 53 deletions(-) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 8f5998313e0..c481a0a4239 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -112,7 +112,10 @@ import { } from '../../shared/constants' import { advertisedUrlWatcher } from '../ports/advertised-url-watcher' import { makePaneKey } from '../../shared/stable-pane-id' -import { SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV } from '../../shared/setup-agent-sequencing' +import { + SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, + SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV +} from '../../shared/setup-agent-sequencing' import type { AgentSessionExecutionClaim, AgentSessionSurfaceBinding @@ -5674,7 +5677,9 @@ describe('OrcaRuntimeService', () => { 1, expect.objectContaining({ cwd: '/remote/mobile-setup', - command: expect.stringContaining('exec claude'), + env: expect.objectContaining({ + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude') + }), worktreeId: result.worktree.id }) ) @@ -5688,14 +5693,20 @@ describe('OrcaRuntimeService', () => { worktreeId: result.worktree.id }) ) - const startupCommand = (spawn.mock.calls[0]![0] as { command: string }).command + const startup = spawn.mock.calls[0]![0] as { + command: string + env: Record + } + const startupCommand = startup.command + const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]! const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command - const nonceMatch = startupCommand.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) + const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() const markerPath = `/remote/repo/.git/worktrees/mobile-setup/orca/setup-runner.sh.${nonceMatch![1]}.done` + expect(startupCommand.length).toBeLessThan(256) expect(setupCommand).toContain('printf') expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) - expect(startupCommand).toContain(markerPath) + expect(startupScript).toContain(markerPath) expect(setupCommand).toContain(markerPath) expect(revealTerminalSession).toHaveBeenLastCalledWith( result.worktree.id, @@ -39833,12 +39844,18 @@ describe('OrcaRuntimeService', () => { ) // Why: setup is provisioned fire-and-forget; the wait-for-setup guarantee comes from the shell nonce/marker, not JS spawn ordering. await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2)) - const startupCommand = (spawn.mock.calls[0]![0] as { command: string }).command + const startup = spawn.mock.calls[0]![0] as { + command: string + env: Record + } + const startupCommand = startup.command + const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]! const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command - const nonceMatch = startupCommand.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) + const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() - expect(startupCommand).toContain('exec claude') - expect(startupCommand).toContain('/mnt/c/tmp/repo/.git/orca/setup-runner.sh') + expect(startupCommand.length).toBeLessThan(256) + expect(startupScript).toContain('exec claude') + expect(startupScript).toContain('/mnt/c/tmp/repo/.git/orca/setup-runner.sh') expect(setupCommand).toContain('bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh') expect(setupCommand).toContain('printf') expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) @@ -41117,7 +41134,9 @@ describe('OrcaRuntimeService', () => { 1, expect.objectContaining({ cwd: '/tmp/workspaces/runtime-startup-setup-split', - command: expect.stringContaining('codex'), + env: expect.objectContaining({ + [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: expect.stringContaining('codex') + }), worktreeId: result.worktree.id }) ) @@ -41134,15 +41153,21 @@ describe('OrcaRuntimeService', () => { worktreeId: result.worktree.id }) ) - const startupCommand = (spawn.mock.calls[0]![0] as { command: string }).command + const startup = spawn.mock.calls[0]![0] as { + command: string + env: Record + } + const startupCommand = startup.command + const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]! const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command - const nonceMatch = startupCommand.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) + const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() const markerPath = `/tmp/repo/.git/orca/setup-runner.sh.${nonceMatch![1]}.done` - expect(startupCommand).toContain('--dangerously-bypass-approvals-and-sandbox') + expect(startupCommand.length).toBeLessThan(256) + expect(startupScript).toContain('--dangerously-bypass-approvals-and-sandbox') expect(setupCommand).toContain('printf') expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) - expect(startupCommand).toContain(markerPath) + expect(startupScript).toContain(markerPath) expect(setupCommand).toContain(markerPath) const mainEnv = (spawn.mock.calls[0]![0] as { env?: Record }).env ?? {} const setupEnv = (spawn.mock.calls[1]![0] as { env?: Record }).env ?? {} diff --git a/src/renderer/src/lib/worktree-activation.test.ts b/src/renderer/src/lib/worktree-activation.test.ts index e99c70fadef..cdebf649389 100644 --- a/src/renderer/src/lib/worktree-activation.test.ts +++ b/src/renderer/src/lib/worktree-activation.test.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines -- Why: these activation cases share one mock store and assert ordering across startup, setup, issue commands, and default tabs. */ import { afterEach, describe, expect, it, vi } from 'vitest' import type { SetupScriptLaunchMode } from '../../../shared/types' +import { SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from '../../../shared/setup-agent-sequencing' import { activateAndRevealWorktree, ensureWorktreeHasInitialTerminal } from './worktree-activation' import { resetHookCommandDelayedDeliveryForTests } from './hook-command-delayed-delivery' import { useAppStore } from '@/store' @@ -698,13 +699,19 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.queueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: expect.stringContaining('Timed out waiting for setup before starting agent.') + env: expect.objectContaining({ + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining( + 'Timed out waiting for setup before starting agent.' + ) + }) }) ) expect(store.queueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: expect.stringContaining('exec claude') + env: expect.objectContaining({ + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude') + }) }) ) expect(store.queueTabStartupCommand).toHaveBeenCalledWith( @@ -765,7 +772,9 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.queueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: expect.stringContaining('exec claude') + env: expect.objectContaining({ + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude') + }) }) ) expect(store.queueTabSetupSplit).toHaveBeenCalledWith('tab-1', { @@ -799,7 +808,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.queueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: expect.stringContaining('/mnt/c/repo/.git/orca/setup-runner.sh') + env: expect.objectContaining({ + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining( + '/mnt/c/repo/.git/orca/setup-runner.sh' + ) + }) }) ) expect(store.queueTabSetupSplit).toHaveBeenCalledWith('tab-1', { diff --git a/src/shared/setup-agent-sequencing.test.ts b/src/shared/setup-agent-sequencing.test.ts index 61dc001e627..5bff3422f95 100644 --- a/src/shared/setup-agent-sequencing.test.ts +++ b/src/shared/setup-agent-sequencing.test.ts @@ -11,7 +11,8 @@ import { createSetupAgentSequenceNonce, getSetupAgentSequenceShellForTests, resolveSetupAgentSequenceLaunchCommand, - SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV + SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, + SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from './setup-agent-sequencing' import { DEFAULT_SETUP_AGENT_STARTUP_POLICY, @@ -66,23 +67,42 @@ describe('createSequencedSetupAgentCommands', () => { expect(result.setupCommand).toContain( 'mv -f /repo/.git/orca/setup-runner.sh.nonce-123.done.tmp' ) - expect(result.startupCommand).toMatch(/^bash -lc /) - expect(result.startupCommand).toContain('deadline=$((SECONDS + 9))') - expect(result.startupCommand).not.toContain('date +%s') - expect(result.startupCommand).toContain('Waiting for setup to finish before starting agent...') - expect(result.startupCommand).toContain('[ "$seen" = nonce-123 ]') - expect(result.startupCommand).toContain( + const startupScript = result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV] + expect(result.startupCommand).toBe( + `bash -lc 'eval "$${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}"'` + ) + expect(startupScript).toContain('deadline=$((SECONDS + 9))') + expect(startupScript).not.toContain('date +%s') + expect(startupScript).toContain('Waiting for setup to finish before starting agent...') + expect(startupScript).toContain('[ "$seen" = nonce-123 ]') + expect(startupScript).toContain( 'rm -f /repo/.git/orca/setup-runner.sh.nonce-123.done /repo/.git/orca/setup-runner.sh.nonce-123.done.tmp' ) - expect(result.startupCommand).toContain('exec codex') - expect(result.startupCommand).toContain('fix bug') + expect(startupScript).toContain('exec codex') + expect(startupScript).toContain('fix bug') expect(result.startupEnv).toEqual( expect.objectContaining({ - [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: "codex 'fix bug'" + [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: "codex 'fix bug'", + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: startupScript }) ) }) + it('keeps the POSIX terminal submission below the canonical input floor', () => { + const result = createSequencedSetupAgentCommands({ + runnerScriptPath: `/repo/${'nested-worktree/'.repeat(100)}setup-runner.sh`, + startupCommand: 'codex', + platform: 'posix', + nonce: 'long-path' + }) + + expect(result.startupCommand.length).toBeLessThan(256) + expect(result.startupCommand).not.toContain('nested-worktree') + expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( + 'nested-worktree' + ) + }) + it('uses launch-specific marker paths for overlapping setup gates', () => { const first = createSequencedSetupAgentCommands({ runnerScriptPath: '/repo/.git/orca/setup-runner.sh', @@ -98,9 +118,13 @@ describe('createSequencedSetupAgentCommands', () => { }) expect(first.setupCommand).toContain('/repo/.git/orca/setup-runner.sh.first-launch.done') - expect(first.startupCommand).toContain('/repo/.git/orca/setup-runner.sh.first-launch.done') + expect(first.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( + '/repo/.git/orca/setup-runner.sh.first-launch.done' + ) expect(second.setupCommand).toContain('/repo/.git/orca/setup-runner.sh.second-launch.done') - expect(second.startupCommand).toContain('/repo/.git/orca/setup-runner.sh.second-launch.done') + expect(second.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( + '/repo/.git/orca/setup-runner.sh.second-launch.done' + ) expect(first.setupCommand).not.toContain('/repo/.git/orca/setup-runner.sh.second-launch.done') expect(second.setupCommand).not.toContain('/repo/.git/orca/setup-runner.sh.first-launch.done') }) @@ -114,8 +138,9 @@ describe('createSequencedSetupAgentCommands', () => { waitTimeoutSeconds: 9 }) - expect(result.startupCommand).toContain("exec codex '\\''fix this; then test'\\''") - expect(result.startupCommand).not.toContain('eval codex') + const startupScript = result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV] + expect(startupScript).toContain("exec codex 'fix this; then test'") + expect(startupScript).not.toContain('eval codex') }) it('preserves POSIX inline environment assignment startup commands', () => { @@ -127,9 +152,10 @@ describe('createSequencedSetupAgentCommands', () => { waitTimeoutSeconds: 9 }) - expect(result.startupCommand).toContain('FOO=bar claude') - expect(result.startupCommand).toContain('exit "$?"') - expect(result.startupCommand).not.toContain('exec FOO=bar claude') + const startupScript = result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV] + expect(startupScript).toContain('FOO=bar claude') + expect(startupScript).toContain('exit "$?"') + expect(startupScript).not.toContain('exec FOO=bar claude') }) it('uses the converted Linux marker path for WSL UNC runners on Windows', () => { @@ -162,7 +188,9 @@ describe('createSequencedSetupAgentCommands', () => { expect(result.setupCommand).toContain( 'bash /remote/repo/.git/worktrees/feature/orca/setup-runner.sh' ) - expect(result.startupCommand).toContain('[ "$seen" = nonce-remote ]') + expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( + '[ "$seen" = nonce-remote ]' + ) }) it('preserves WSL shell metadata when sequencing native Windows runners', () => { @@ -178,7 +206,7 @@ describe('createSequencedSetupAgentCommands', () => { expect(result.setupCommand).toContain( '/mnt/c/repo/.git/orca/setup-runner.sh.nonce-wsl-shell.done' ) - expect(result.startupCommand).toContain( + expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( '/mnt/c/repo/.git/orca/setup-runner.sh.nonce-wsl-shell.done' ) }) @@ -242,12 +270,14 @@ describe('createSequencedSetupAgentCommands', () => { expect(result.setupCommand).toMatch(/^bash -lc /) expect(result.startupCommand).toMatch(/^bash -lc /) expect(result.startupCommand).not.toContain('Invoke-Expression') - expect(result.startupCommand).toContain('eval "$ORCA_SEQUENCED_STARTUP_COMMAND"') + expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( + 'eval "$ORCA_SEQUENCED_STARTUP_COMMAND"' + ) // Why: bash writes and reads the marker here, so it needs the /c/... form of the path. expect(result.setupCommand).toContain( '/c/repo/.git/orca/setup-runner.cmd.nonce-gitbash-cmd.done' ) - expect(result.startupCommand).toContain( + expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( '/c/repo/.git/orca/setup-runner.cmd.nonce-gitbash-cmd.done' ) }) @@ -336,7 +366,10 @@ describe('createSequencedSetupAgentCommands', () => { }) const startupExitPromise = waitForExit( - spawn('bash', ['-lc', commands.startupCommand], { stdio: 'pipe' }) + spawn('bash', ['-lc', commands.startupCommand], { + stdio: 'pipe', + env: { ...process.env, ...commands.startupEnv } + }) ) await sleep(250) expect(readIfExists(logPath)).toBe('') @@ -380,15 +413,19 @@ describe('createSequencedSetupAgentCommands', () => { spawn('bash', ['-lc', commands.setupCommand], { stdio: 'pipe' }) ) const startupExit = await waitForExit( - spawn('bash', ['-lc', commands.startupCommand], { stdio: 'pipe' }) + spawn('bash', ['-lc', commands.startupCommand], { + stdio: 'pipe', + env: { ...process.env, ...commands.startupEnv } + }) ) const setupExit = await setupExitPromise expect(setupExit.code).toBe(0) expect(startupExit.code).toBe(0) expect(readFileSync(logPath, 'utf8')).toBe('setup-done\nagent-start\ncleanup\n') - expect(commands.startupCommand).toContain('eval') - expect(commands.startupCommand).not.toContain('exec printf') + const startupScript = commands.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV] + expect(startupScript).toContain('eval') + expect(startupScript).not.toContain('exec printf') } ) @@ -430,6 +467,7 @@ describe('createSequencedSetupAgentCommands', () => { stdio: 'pipe', env: { ...process.env, + ...commands.startupEnv, [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: `FOO=bar bash ${quoteSh(startupScriptPath)}; printf 'env-cleanup\\n' >> ${quoteSh(logPath)}` } }) @@ -459,7 +497,10 @@ describe('createSequencedSetupAgentCommands', () => { }) const startupExit = await waitForExit( - spawn('bash', ['-lc', commands.startupCommand], { stdio: 'pipe' }) + spawn('bash', ['-lc', commands.startupCommand], { + stdio: 'pipe', + env: { ...process.env, ...commands.startupEnv } + }) ) expect(startupExit.code).toBe(124) diff --git a/src/shared/setup-agent-sequencing.ts b/src/shared/setup-agent-sequencing.ts index 1ec888e3aa6..b93fe9c8306 100644 --- a/src/shared/setup-agent-sequencing.ts +++ b/src/shared/setup-agent-sequencing.ts @@ -9,6 +9,7 @@ import { const DEFAULT_WAIT_TIMEOUT_SECONDS = 2 * 60 * 60 export const SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV = 'ORCA_SEQUENCED_STARTUP_COMMAND' +export const SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV = 'ORCA_SEQUENCED_STARTUP_SCRIPT' export type SequencedSetupAgentCommands = { setupCommand: string @@ -69,16 +70,19 @@ export function createSequencedSetupAgentCommands(args: { } } + const startupScript = buildPosixStartupScript( + args.startupCommand, + markerPath, + nonce, + waitTimeoutSeconds + ) return { setupCommand: buildPosixSetupCommand(resolution.command, markerPath, nonce), - startupCommand: buildPosixStartupCommand( - args.startupCommand, - markerPath, - nonce, - waitTimeoutSeconds - ), + // Why: long worktree paths can push the gate past a PTY's canonical input cap and drop its submit byte. + startupCommand: `bash -lc 'eval "$${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}"'`, startupEnv: { - [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: args.startupCommand + [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: args.startupCommand, + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: startupScript } } } @@ -100,7 +104,7 @@ function buildPosixSetupCommand(setupCommand: string, markerPath: string, nonce: return `bash -lc ${quotePosixArg(script)}` } -function buildPosixStartupCommand( +function buildPosixStartupScript( startupCommand: string, markerPath: string, nonce: string, @@ -135,7 +139,7 @@ function buildPosixStartupCommand( 'done' ].join(' ') - return `bash -lc ${quotePosixArg(script)}` + return script } function buildPosixStartupSuccessCommand(startupCommand: string): string {