diff --git a/src/cliproxy/executor/__tests__/claude-launcher.test.ts b/src/cliproxy/executor/__tests__/claude-launcher.test.ts index e6111e50..3e587ca9 100644 --- a/src/cliproxy/executor/__tests__/claude-launcher.test.ts +++ b/src/cliproxy/executor/__tests__/claude-launcher.test.ts @@ -87,6 +87,16 @@ mock.module('../../quota/quota-manager', () => ({ stopQuotaMonitor: jest.fn(), })); +const mockCleanupLaunchSettings = jest.fn(); +const mockPrepareLaunchSettings = jest.fn().mockReturnValue({ + settingsPath: '/tmp/fake-settings-overlay.json', + cleanup: mockCleanupLaunchSettings, +}); + +mock.module('../launch-settings', () => ({ + prepareLaunchSettings: mockPrepareLaunchSettings, +})); + // ── Subject under test ──────────────────────────────────────────────────────── import { launchClaude } from '../claude-launcher'; @@ -131,6 +141,12 @@ describe('launchClaude', () => { mockSpawn.mockClear(); mockSetupCleanupHandlers.mockClear(); mockEscapeShellArg.mockClear(); + mockCleanupLaunchSettings.mockClear(); + mockPrepareLaunchSettings.mockClear(); + mockPrepareLaunchSettings.mockReturnValue({ + settingsPath: '/tmp/fake-settings-overlay.json', + cleanup: mockCleanupLaunchSettings, + }); }); it('calls spawn with claudeCli and includes --settings arg', async () => { @@ -189,6 +205,16 @@ describe('launchClaude', () => { expect(result).toBe(mockSpawnResult); }); + it('calls cleanup and rethrows when spawn throws synchronously', async () => { + const spawnErr = new Error('ERR_INVALID_ARG_VALUE'); + mockSpawn.mockImplementationOnce(() => { + throw spawnErr; + }); + + await expect(launchClaude(baseContext())).rejects.toThrow('ERR_INVALID_ARG_VALUE'); + expect(mockCleanupLaunchSettings).toHaveBeenCalledTimes(1); + }); + describe('Windows shell escaping', () => { const originalPlatform = process.platform; diff --git a/src/cliproxy/executor/__tests__/launch-settings.test.ts b/src/cliproxy/executor/__tests__/launch-settings.test.ts index 6865cedf..b08d27bc 100644 --- a/src/cliproxy/executor/__tests__/launch-settings.test.ts +++ b/src/cliproxy/executor/__tests__/launch-settings.test.ts @@ -91,6 +91,17 @@ describe('buildLaunchSettingsOverlay', () => { 'http://127.0.0.1:50118/api/provider/codex' ); }); + + it('falls back to an env-only overlay when the settings file is corrupt', () => { + fs.writeFileSync(settingsPath, '{ not valid json'); + const { settings, changed } = buildLaunchSettingsOverlay(settingsPath, { + ANTHROPIC_BASE_URL: 'http://127.0.0.1:50118/api/provider/codex', + } as NodeJS.ProcessEnv); + expect(changed).toBe(true); + expect((settings.env as Record).ANTHROPIC_BASE_URL).toBe( + 'http://127.0.0.1:50118/api/provider/codex' + ); + }); }); describe('prepareLaunchSettings', () => { @@ -127,4 +138,20 @@ describe('prepareLaunchSettings', () => { result.cleanup(); expect(fs.existsSync(settingsPath)).toBe(true); }); + + it('writes the overlay with 0600 file mode inside a 0700 dir (POSIX)', () => { + if (process.platform === 'win32') return; // chmod modes are not enforced on Windows + writePersisted({ + env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex' }, + }); + const result = prepareLaunchSettings(settingsPath, { + ANTHROPIC_BASE_URL: 'http://127.0.0.1:50118/api/provider/codex', + } as NodeJS.ProcessEnv); + try { + expect(fs.statSync(result.settingsPath).mode & 0o777).toBe(0o600); + expect(fs.statSync(path.dirname(result.settingsPath)).mode & 0o777).toBe(0o700); + } finally { + result.cleanup(); + } + }); }); diff --git a/src/cliproxy/executor/claude-launcher.ts b/src/cliproxy/executor/claude-launcher.ts index ca539971..ff189bbf 100644 --- a/src/cliproxy/executor/claude-launcher.ts +++ b/src/cliproxy/executor/claude-launcher.ts @@ -144,20 +144,28 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise void; } +// SIBLING HELPER: src/cliproxy/executor/launch-settings.ts (prepareLaunchSettings) +// solves the same problem by OVERLAYING resolved routing values instead of +// stripping them. This strip-based variant is required where callers deliberately +// delete a routing key (e.g. ANTHROPIC_API_KEY in settings-flow) and need it +// ABSENT from the launch settings. Do not unify without an explicit force-absent +// key list — see issue #1609. export function createOpenAICompatLaunchSettings( settingsPath: string, settings: Settings diff --git a/src/utils/shell-executor.ts b/src/utils/shell-executor.ts index 58d459f6..68a9f1f6 100644 --- a/src/utils/shell-executor.ts +++ b/src/utils/shell-executor.ts @@ -40,6 +40,10 @@ export const ANTHROPIC_ROUTING_ENV_KEYS = [ 'ANTHROPIC_API_KEY', ]; const ANTHROPIC_ROUTING_ENV_KEY_SET = new Set(ANTHROPIC_ROUTING_ENV_KEYS); +// NOTE: This is the intentional routing-overlay SUPERSET of model env keys +// (includes ANTHROPIC_SMALL_FAST_MODEL). A separate 4-key `ANTHROPIC_MODEL_ENV_KEYS` +// exists in src/shared/extended-context-utils.ts (re-exported as MODEL_ENV_VAR_KEYS). +// The two are NOT interchangeable — import deliberately by purpose. See issue #1609. export const ANTHROPIC_MODEL_ENV_KEYS = [ 'ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL',