mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(claude): scrub SDK child env and localize pending launch
This commit is contained in:
@@ -41,4 +41,23 @@ describe('Claude child process environment', () => {
|
||||
SAFE_VALUE: 'preserved'
|
||||
})
|
||||
})
|
||||
|
||||
it('can strip child-session stamps reintroduced by a full SDK launch overlay', () => {
|
||||
expect(
|
||||
buildClaudeChildProcessEnv(
|
||||
{
|
||||
CLAUDE_CODE_CHILD_SESSION: 'configured-child-session',
|
||||
CLAUDE_CODE_SESSION_ID: 'configured-session',
|
||||
CLAUDE_CODE_BRIDGE_SESSION_ID: 'configured-bridge-session'
|
||||
},
|
||||
{
|
||||
scrubConfiguredChildSessionStamps: true,
|
||||
inheritedEnv: {
|
||||
CLAUDE_CODE_CHILD_SESSION: 'inherited-child-session',
|
||||
SAFE_VALUE: 'preserved'
|
||||
}
|
||||
}
|
||||
)
|
||||
).toEqual({ SAFE_VALUE: 'preserved' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,9 +16,27 @@ function cloneProcessEnv(source: NodeJS.ProcessEnv): Record<string, string> {
|
||||
return env
|
||||
}
|
||||
|
||||
function stripClaudeChildSessionStamps(
|
||||
env: Record<string, string>,
|
||||
platform: NodeJS.Platform
|
||||
): Record<string, string> {
|
||||
for (const key of CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS) {
|
||||
for (const envKey of Object.keys(env)) {
|
||||
if (envKey === key || (platform === 'win32' && envKey.toUpperCase() === key)) {
|
||||
delete env[envKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
export function buildClaudeChildProcessEnv(
|
||||
configuredEnv: Record<string, string> = {},
|
||||
options: { inheritedEnv?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {}
|
||||
options: {
|
||||
inheritedEnv?: NodeJS.ProcessEnv
|
||||
platform?: NodeJS.Platform
|
||||
scrubConfiguredChildSessionStamps?: boolean
|
||||
} = {}
|
||||
): Record<string, string> {
|
||||
const inheritedEnv = options.inheritedEnv ?? process.env
|
||||
const platform = options.platform ?? process.platform
|
||||
@@ -43,12 +61,9 @@ export function buildClaudeChildProcessEnv(
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS) {
|
||||
for (const inheritedKey of Object.keys(env)) {
|
||||
if (inheritedKey === key || (platform === 'win32' && inheritedKey.toUpperCase() === key)) {
|
||||
delete env[inheritedKey]
|
||||
}
|
||||
}
|
||||
if (options.scrubConfiguredChildSessionStamps) {
|
||||
return stripClaudeChildSessionStamps({ ...env, ...configuredEnv }, platform)
|
||||
}
|
||||
return Object.assign(env, configuredEnv)
|
||||
stripClaudeChildSessionStamps(env, platform)
|
||||
return { ...env, ...configuredEnv }
|
||||
}
|
||||
|
||||
@@ -150,7 +150,10 @@ describe('Claude stream-json connection', () => {
|
||||
launchFor(scenario, {
|
||||
CLAUDE_CONFIG_DIR: '/accounts/managed/home',
|
||||
ANTHROPIC_AUTH_TOKEN: 'configured-token',
|
||||
ORCA_AGENT_SESSION_SPAWN_TOKEN: 'spawn-9'
|
||||
ORCA_AGENT_SESSION_SPAWN_TOKEN: 'spawn-9',
|
||||
CLAUDE_CODE_CHILD_SESSION: 'configured-child-session',
|
||||
CLAUDE_CODE_SESSION_ID: 'configured-session',
|
||||
CLAUDE_CODE_BRIDGE_SESSION_ID: 'configured-bridge-session'
|
||||
})
|
||||
)
|
||||
|
||||
@@ -165,6 +168,8 @@ describe('Claude stream-json connection', () => {
|
||||
expect(env.ORCA_CONNECTION_MARKER).toBe('inherited')
|
||||
expect(env.ANTHROPIC_API_KEY).toBeUndefined()
|
||||
expect(env.CLAUDE_CODE_CHILD_SESSION).toBeUndefined()
|
||||
expect(env.CLAUDE_CODE_SESSION_ID).toBeUndefined()
|
||||
expect(env.CLAUDE_CODE_BRIDGE_SESSION_ID).toBeUndefined()
|
||||
// Two SDK mutations of the child env, pinned so a bump cannot change them unseen.
|
||||
expect(env.CLAUDE_CODE_ENTRYPOINT).toBe('sdk-ts')
|
||||
expect(env.NODE_OPTIONS).toBeUndefined()
|
||||
|
||||
@@ -107,7 +107,7 @@ export async function openClaudeStreamJsonConnection(
|
||||
cwd: launch.cwd,
|
||||
// Why env is never omitted: the SDK inherits process.env when it is, which is
|
||||
// exactly the ambient ANTHROPIC_* auth leak this lane already shipped once.
|
||||
env: buildClaudeChildProcessEnv(launch.env),
|
||||
env: buildClaudeChildProcessEnv(launch.env, { scrubConfiguredChildSessionStamps: true }),
|
||||
pathToClaudeCodeExecutable: launch.pathToClaudeCodeExecutable,
|
||||
spawnClaudeCodeProcess: spawner.spawn,
|
||||
...(handlers.canUseTool ? { canUseTool: handlers.canUseTool } : {}),
|
||||
|
||||
@@ -9,7 +9,9 @@ const {
|
||||
openSettingsPageMock,
|
||||
openSettingsTargetMock,
|
||||
useDetectedAgentsMock,
|
||||
useStructuredAgentSessionCreateMock
|
||||
useStructuredAgentSessionCreateMock,
|
||||
useStructuredCodexLaunchStatusMock,
|
||||
translateMock
|
||||
} = vi.hoisted(() => ({
|
||||
shortcutLabelMock: vi.fn<() => string | null>(),
|
||||
storeState: {
|
||||
@@ -29,7 +31,20 @@ const {
|
||||
supported: true,
|
||||
creating: false,
|
||||
create: vi.fn(async () => true)
|
||||
}))
|
||||
})),
|
||||
useStructuredCodexLaunchStatusMock: vi.fn(() => 'idle' as 'idle' | 'pending' | 'failed'),
|
||||
translateMock: vi.fn((key: string, fallback: string, values?: Record<string, string>): string => {
|
||||
const template =
|
||||
key === 'auto.components.tab.bar.QuickLaunchButton.ec2adf093e'
|
||||
? 'Launch {{value0}} in a new terminal'
|
||||
: key === 'auto.components.tab.bar.QuickLaunchButton.startingCodexChat'
|
||||
? 'Starting Codex chat (localized)…'
|
||||
: fallback
|
||||
return Object.entries(values ?? {}).reduce(
|
||||
(text, [name, value]) => text.replace(`{{${name}}}`, value),
|
||||
template
|
||||
)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useDetectedAgents', () => ({
|
||||
@@ -44,6 +59,10 @@ vi.mock('../native-chat/use-structured-agent-session-create', () => ({
|
||||
useStructuredAgentSessionCreate: useStructuredAgentSessionCreateMock
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/structured-agent-session-launch', () => ({
|
||||
useStructuredCodexLaunchStatus: useStructuredCodexLaunchStatusMock
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => {
|
||||
const useAppStore = Object.assign(
|
||||
(selector: (state: typeof storeState) => unknown) => {
|
||||
@@ -85,13 +104,7 @@ vi.mock('@/components/ui/dropdown-menu', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
translate: (_key: string, fallback: string, values?: Record<string, string>) =>
|
||||
Object.entries(values ?? {}).reduce(
|
||||
(text, [key, value]) => text.replace(`{{${key}}}`, value),
|
||||
fallback
|
||||
)
|
||||
}))
|
||||
vi.mock('@/i18n/i18n', () => ({ translate: translateMock }))
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
@@ -128,6 +141,9 @@ beforeEach(() => {
|
||||
shortcutLabelMock.mockReturnValue(null)
|
||||
useDetectedAgentsMock.mockClear()
|
||||
useStructuredAgentSessionCreateMock.mockClear()
|
||||
useStructuredCodexLaunchStatusMock.mockReset()
|
||||
useStructuredCodexLaunchStatusMock.mockReturnValue('idle')
|
||||
translateMock.mockClear()
|
||||
openSettingsPageMock.mockReset()
|
||||
openSettingsTargetMock.mockReset()
|
||||
storeState.settings.defaultTuiAgent = 'codex'
|
||||
@@ -227,6 +243,16 @@ describe('QuickLaunchAgentMenuItems', () => {
|
||||
expect(useStructuredAgentSessionCreateMock).toHaveBeenCalledWith('worktree-1', 'claude')
|
||||
expect(useStructuredAgentSessionCreateMock).toHaveBeenCalledWith('worktree-1', 'codex')
|
||||
})
|
||||
|
||||
it('localizes pending Codex text and interpolates the existing launch tooltip', () => {
|
||||
useStructuredCodexLaunchStatusMock.mockReturnValue('pending')
|
||||
|
||||
const html = renderAgentMenuItems()
|
||||
|
||||
expect(html).toContain('Starting Codex chat (localized)…')
|
||||
expect(html).toContain('title="Launch Codex in a new terminal"')
|
||||
expect(html).not.toContain('{{value0}}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldShowLaunchWatchdogTimeout', () => {
|
||||
|
||||
@@ -210,7 +210,12 @@ function QuickLaunchAgentMenuItemsInner({
|
||||
: null
|
||||
const isStructuredCodexPending =
|
||||
agent === 'codex' && structuredCodexLaunchStatus === 'pending'
|
||||
const menuLabel = isStructuredCodexPending ? 'Starting Codex chat…' : label
|
||||
const menuLabel = isStructuredCodexPending
|
||||
? translate(
|
||||
'auto.components.tab.bar.QuickLaunchButton.startingCodexChat',
|
||||
'Starting Codex chat…'
|
||||
)
|
||||
: label
|
||||
const showsDefaultAgentShortcut =
|
||||
newAgentShortcut !== null && defaultAgent !== 'blank' && agent === defaultAgent
|
||||
return (
|
||||
@@ -221,10 +226,8 @@ function QuickLaunchAgentMenuItemsInner({
|
||||
className="gap-2 rounded-[7px] px-2 py-1.5 text-[12px] leading-5 font-medium"
|
||||
title={translate(
|
||||
'auto.components.tab.bar.QuickLaunchButton.ec2adf093e',
|
||||
isStructuredCodexPending
|
||||
? 'Starting Codex chat…'
|
||||
: 'Launch {{value0}} in a new terminal',
|
||||
isStructuredCodexPending ? undefined : { value0: label }
|
||||
'Launch {{value0}} in a new terminal',
|
||||
{ value0: label }
|
||||
)}
|
||||
>
|
||||
{isStructuredCodexPending ? (
|
||||
|
||||
@@ -3330,7 +3330,8 @@
|
||||
"8dea9b5cdf": "No enabled agents",
|
||||
"f378201fbd": "Could not create chat session",
|
||||
"a847779775": "Start {{value0}} without a terminal",
|
||||
"0ea6a78efb": "Chat session"
|
||||
"0ea6a78efb": "Chat session",
|
||||
"startingCodexChat": "Starting Codex chat…"
|
||||
},
|
||||
"RecentTabSwitcher": {
|
||||
"329638ff6f": "Switch Tab",
|
||||
|
||||
Reference in New Issue
Block a user