mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 16:19:27 +00:00
Merge pull request #1112 from fatjester/fix-azure-openai-proxy
fix(proxy): avoid settings override and nested reasoning for openai-compat
This commit is contained in:
+4
-2
@@ -82,6 +82,7 @@ import { tryHandleRootCommand } from './commands/root-command-router';
|
||||
// Import extracted utility functions
|
||||
import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/shell-executor';
|
||||
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
||||
import { createOpenAICompatLaunchSettings } from './utils/openai-compat-launch-settings';
|
||||
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
||||
import { createLogger } from './services/logging';
|
||||
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
|
||||
@@ -1459,10 +1460,11 @@ async function main(): Promise<void> {
|
||||
),
|
||||
};
|
||||
delete proxyEnv.ANTHROPIC_API_KEY;
|
||||
const launchSettings = createOpenAICompatLaunchSettings(expandedSettingsPath, settings);
|
||||
|
||||
const launchArgs = [
|
||||
'--settings',
|
||||
expandedSettingsPath,
|
||||
launchSettings.settingsPath,
|
||||
...appendThirdPartyWebSearchToolArgs(browserArgs),
|
||||
];
|
||||
const traceEnv = createWebSearchTraceContext({
|
||||
@@ -1473,7 +1475,7 @@ async function main(): Promise<void> {
|
||||
settingsPath: expandedSettingsPath,
|
||||
});
|
||||
|
||||
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv });
|
||||
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }, launchSettings.cleanup);
|
||||
return;
|
||||
}
|
||||
const launchArgs = [
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
stripAnthropicRoutingEnv,
|
||||
stripClaudeCodeEnv,
|
||||
} from '../utils/shell-executor';
|
||||
import { createOpenAICompatLaunchSettings } from '../utils/openai-compat-launch-settings';
|
||||
import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance';
|
||||
import {
|
||||
appendThirdPartyImageAnalysisToolArgs,
|
||||
@@ -255,8 +256,12 @@ export class HeadlessExecutor {
|
||||
// Smart slash command detection and preservation
|
||||
const processedPrompt = this._processSlashCommand(enhancedPrompt);
|
||||
|
||||
const launchSettings = openAICompatProfile
|
||||
? createOpenAICompatLaunchSettings(settingsPath, settings)
|
||||
: { settingsPath, cleanup: () => {} };
|
||||
|
||||
// Prepare arguments
|
||||
const args: string[] = ['-p', processedPrompt, '--settings', settingsPath];
|
||||
const args: string[] = ['-p', processedPrompt, '--settings', launchSettings.settingsPath];
|
||||
|
||||
// Always use stream-json for real-time progress visibility
|
||||
args.push('--output-format', 'stream-json', '--verbose');
|
||||
@@ -368,6 +373,7 @@ export class HeadlessExecutor {
|
||||
imageAnalysisEnv,
|
||||
runtimeEnvVars,
|
||||
traceEnv,
|
||||
launchCleanup: launchSettings.cleanup,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -388,6 +394,7 @@ export class HeadlessExecutor {
|
||||
imageAnalysisEnv?: Record<string, string>;
|
||||
runtimeEnvVars?: NodeJS.ProcessEnv;
|
||||
traceEnv?: Record<string, string>;
|
||||
launchCleanup?: () => void;
|
||||
}
|
||||
): Promise<ExecutionResult> {
|
||||
const {
|
||||
@@ -401,6 +408,7 @@ export class HeadlessExecutor {
|
||||
imageAnalysisEnv = {},
|
||||
runtimeEnvVars = {},
|
||||
traceEnv = {},
|
||||
launchCleanup = () => {},
|
||||
} = ctx;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -438,6 +446,14 @@ export class HeadlessExecutor {
|
||||
let progressInterval: NodeJS.Timeout | undefined;
|
||||
const messages: StreamMessage[] = [];
|
||||
let timedOut = false;
|
||||
let cleanedUp = false;
|
||||
const cleanupLaunchArtifacts = () => {
|
||||
if (cleanedUp) {
|
||||
return;
|
||||
}
|
||||
cleanedUp = true;
|
||||
launchCleanup();
|
||||
};
|
||||
|
||||
// Setup signal handlers for cleanup
|
||||
const cleanupHandler = () => {
|
||||
@@ -496,6 +512,7 @@ export class HeadlessExecutor {
|
||||
|
||||
// Handle completion
|
||||
proc.on('close', (exitCode: number | null) => {
|
||||
cleanupLaunchArtifacts();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (progressInterval) {
|
||||
@@ -588,6 +605,7 @@ export class HeadlessExecutor {
|
||||
|
||||
// Handle errors
|
||||
proc.on('error', (error: Error) => {
|
||||
cleanupLaunchArtifacts();
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
reject(new Error(`Failed to execute Claude CLI: ${error.message}`));
|
||||
});
|
||||
|
||||
@@ -362,7 +362,7 @@ function transformToolChoice(
|
||||
function mapThinkingToReasoning(
|
||||
thinking: AnthropicThinking | undefined,
|
||||
outputConfig: AnthropicOutputConfig | undefined
|
||||
): Pick<ProxyOpenAIRequest, 'reasoning' | 'reasoning_effort'> {
|
||||
): Pick<ProxyOpenAIRequest, 'reasoning_effort'> {
|
||||
if (!thinking || thinking.type === 'disabled') {
|
||||
return {};
|
||||
}
|
||||
@@ -371,10 +371,6 @@ function mapThinkingToReasoning(
|
||||
const effort = toOpenAIEffort(resolveOutputConfigEffort(outputConfig) ?? 'high');
|
||||
return {
|
||||
reasoning_effort: effort,
|
||||
reasoning: {
|
||||
enabled: true,
|
||||
effort,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -389,10 +385,6 @@ function mapThinkingToReasoning(
|
||||
|
||||
return {
|
||||
reasoning_effort: effort,
|
||||
reasoning: {
|
||||
enabled: true,
|
||||
effort,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import type { Settings } from '../types/config';
|
||||
import { stripAnthropicRoutingEnv } from './shell-executor';
|
||||
|
||||
export interface OpenAICompatLaunchSettings {
|
||||
settingsPath: string;
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
export function createOpenAICompatLaunchSettings(
|
||||
settingsPath: string,
|
||||
settings: Settings
|
||||
): OpenAICompatLaunchSettings {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-openai-compat-settings-'));
|
||||
fs.chmodSync(tempDir, 0o700);
|
||||
|
||||
const launchSettings = JSON.parse(JSON.stringify(settings)) as Settings;
|
||||
const sanitizedEnv = Object.fromEntries(
|
||||
Object.entries(stripAnthropicRoutingEnv({ ...(launchSettings.env ?? {}) })).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
||||
)
|
||||
);
|
||||
|
||||
if (Object.keys(sanitizedEnv).length > 0) {
|
||||
launchSettings.env = sanitizedEnv;
|
||||
} else {
|
||||
delete launchSettings.env;
|
||||
}
|
||||
|
||||
const launchSettingsPath = path.join(tempDir, path.basename(settingsPath));
|
||||
fs.writeFileSync(launchSettingsPath, JSON.stringify(launchSettings, null, 2) + '\n', {
|
||||
encoding: 'utf8',
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
let cleanedUp = false;
|
||||
const cleanup = (): void => {
|
||||
if (cleanedUp) {
|
||||
return;
|
||||
}
|
||||
cleanedUp = true;
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
return {
|
||||
settingsPath: launchSettingsPath,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
@@ -208,7 +208,8 @@ export function getWindowsEscapedCommandShell(): SpawnOptions['shell'] {
|
||||
export function execClaude(
|
||||
claudeCli: string,
|
||||
args: string[],
|
||||
envVars: NodeJS.ProcessEnv | null = null
|
||||
envVars: NodeJS.ProcessEnv | null = null,
|
||||
onExitCleanup?: () => void
|
||||
): void {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
|
||||
@@ -286,6 +287,17 @@ export function execClaude(
|
||||
});
|
||||
}
|
||||
|
||||
let cleanedUp = false;
|
||||
const runExitCleanup = (): void => {
|
||||
if (cleanedUp) {
|
||||
return;
|
||||
}
|
||||
cleanedUp = true;
|
||||
onExitCleanup?.();
|
||||
};
|
||||
child.once('exit', runExitCleanup);
|
||||
child.once('error', runExitCleanup);
|
||||
|
||||
wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'EACCES') {
|
||||
console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
|
||||
|
||||
@@ -299,7 +299,58 @@ describe('openai proxy request routing', () => {
|
||||
expect(bodies[0]?.body).toMatchObject({
|
||||
model: 'deepseek-reasoner',
|
||||
reasoning_effort: 'high',
|
||||
reasoning: { enabled: true, effort: 'high' },
|
||||
});
|
||||
expect((bodies[0]?.body as { reasoning?: unknown } | undefined)?.reasoning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('forwards adaptive thinking to openai-profile upstreams via reasoning_effort only', async () => {
|
||||
const hits: string[] = [];
|
||||
const bodies: Array<{ label: string; body: unknown }> = [];
|
||||
const upstreamPort = await startMockUpstream('openai', hits, bodies);
|
||||
|
||||
const settingsPath = writeSettings('openai', {
|
||||
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'openai_token',
|
||||
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.ccs', 'config.json'),
|
||||
JSON.stringify({ profiles: { openai: settingsPath } }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const profile: OpenAICompatProfileConfig = {
|
||||
profileName: 'openai',
|
||||
settingsPath,
|
||||
baseUrl: `http://127.0.0.1:${upstreamPort}`,
|
||||
apiKey: 'openai_token',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4.1',
|
||||
};
|
||||
proxyServer = startOpenAICompatProxyServer({
|
||||
profile,
|
||||
port: 0,
|
||||
authToken: 'test-proxy-token',
|
||||
});
|
||||
proxyPort = await waitForServerListening(proxyServer);
|
||||
|
||||
const response = await requestProxy({
|
||||
model: 'gpt-4.1',
|
||||
thinking: { type: 'adaptive' },
|
||||
output_config: { effort: 'max' },
|
||||
messages: [{ role: 'user', content: 'think adaptively' }],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({
|
||||
content: [{ type: 'text', text: 'Reply from openai' }],
|
||||
});
|
||||
expect(hits).toEqual(['openai']);
|
||||
expect(bodies[0]?.body).toMatchObject({
|
||||
model: 'gpt-4.1',
|
||||
reasoning_effort: 'high',
|
||||
});
|
||||
expect((bodies[0]?.body as { reasoning?: unknown } | undefined)?.reasoning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('ProxyRequestTransformer regressions', () => {
|
||||
});
|
||||
|
||||
expect(result.reasoning_effort).toBe('high');
|
||||
expect(result.reasoning).toEqual({ enabled: true, effort: 'high' });
|
||||
expect(result.reasoning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('explicitly normalizes anthropic xhigh adaptive effort for OpenAI-compatible upstreams', () => {
|
||||
@@ -38,7 +38,7 @@ describe('ProxyRequestTransformer regressions', () => {
|
||||
});
|
||||
|
||||
expect(result.reasoning_effort).toBe('high');
|
||||
expect(result.reasoning).toEqual({ enabled: true, effort: 'high' });
|
||||
expect(result.reasoning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects unsupported thinking types instead of silently dropping them', () => {
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('ProxyRequestTransformer', () => {
|
||||
|
||||
expect(result.stream).toBe(true);
|
||||
expect(result.reasoning_effort).toBe('high');
|
||||
expect(result.reasoning).toEqual({ enabled: true, effort: 'high' });
|
||||
expect(result.reasoning).toBeUndefined();
|
||||
expect(result.max_tokens).toBe(1024);
|
||||
expect(result.temperature).toBe(0.2);
|
||||
expect(result.top_p).toBe(0.9);
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { mutateUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||
import { stopOpenAICompatProxy } from '../../../src/proxy/proxy-daemon';
|
||||
|
||||
const BROWSER_PROMPT_SNIPPET = 'prefer the CCS MCP Browser tool';
|
||||
setDefaultTimeout(30000);
|
||||
@@ -30,6 +31,14 @@ function runCcs(args: string[], env: NodeJS.ProcessEnv): RunResult {
|
||||
};
|
||||
}
|
||||
|
||||
function readLaunchedArgs(argsLogPath: string): string[] {
|
||||
return fs
|
||||
.readFileSync(argsLogPath, 'utf8')
|
||||
.split('\n')
|
||||
.map((arg) => arg.trim())
|
||||
.filter((arg) => arg.length > 0);
|
||||
}
|
||||
|
||||
function reserveClosedPort(): number {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
@@ -83,6 +92,8 @@ describe('settings profile browser launch', () => {
|
||||
let fakeClaudePath = '';
|
||||
let claudeArgsLogPath = '';
|
||||
let claudeEnvLogPath = '';
|
||||
let claudeSettingsPathLogPath = '';
|
||||
let claudeSettingsSnapshotPath = '';
|
||||
let browserProfileDir = '';
|
||||
let devtoolsServer: ChildProcess | undefined;
|
||||
let baseEnv: NodeJS.ProcessEnv;
|
||||
@@ -98,6 +109,8 @@ describe('settings profile browser launch', () => {
|
||||
fakeClaudePath = path.join(tmpHome, 'fake-claude.sh');
|
||||
claudeArgsLogPath = path.join(tmpHome, 'claude-args.txt');
|
||||
claudeEnvLogPath = path.join(tmpHome, 'claude-env.txt');
|
||||
claudeSettingsPathLogPath = path.join(tmpHome, 'claude-settings-path.txt');
|
||||
claudeSettingsSnapshotPath = path.join(tmpHome, 'claude-settings-snapshot.json');
|
||||
browserProfileDir = path.join(tmpHome, 'chrome-user-data');
|
||||
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
@@ -129,6 +142,19 @@ describe('settings profile browser launch', () => {
|
||||
fakeClaudePath,
|
||||
`#!/bin/sh
|
||||
printf "%s\n" "$@" > "${claudeArgsLogPath}"
|
||||
settings_path=""
|
||||
prev=""
|
||||
for arg in "$@"; do
|
||||
if [ "$prev" = "--settings" ]; then
|
||||
settings_path="$arg"
|
||||
break
|
||||
fi
|
||||
prev="$arg"
|
||||
done
|
||||
printf "%s" "$settings_path" > "${claudeSettingsPathLogPath}"
|
||||
if [ -n "$settings_path" ] && [ -f "$settings_path" ]; then
|
||||
cat "$settings_path" > "${claudeSettingsSnapshotPath}"
|
||||
fi
|
||||
{
|
||||
printf "userDataDir=%s\n" "$CCS_BROWSER_USER_DATA_DIR"
|
||||
printf "legacyProfileDir=%s\n" "$CCS_BROWSER_PROFILE_DIR"
|
||||
@@ -167,7 +193,7 @@ exit 0
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
if (devtoolsServer) {
|
||||
devtoolsServer.kill();
|
||||
devtoolsServer = undefined;
|
||||
@@ -176,6 +202,7 @@ exit 0
|
||||
return;
|
||||
}
|
||||
|
||||
await stopOpenAICompatProxy();
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -257,6 +284,76 @@ exit 0
|
||||
}
|
||||
});
|
||||
|
||||
it('passes a sanitized settings copy for local OpenAI-compatible proxy launches', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'profile-token',
|
||||
ANTHROPIC_API_KEY: 'profile-api-key',
|
||||
ANTHROPIC_MODEL: 'gpt-5.4',
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS: '12345',
|
||||
},
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: 'Read',
|
||||
hooks: [{ type: 'command', command: 'echo keep-profile-settings' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
);
|
||||
|
||||
const result = runCcs(['glm', 'smoke'], baseEnv);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const launchedArgs = readLaunchedArgs(claudeArgsLogPath);
|
||||
const settingsIndex = launchedArgs.indexOf('--settings');
|
||||
expect(settingsIndex).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const launchSettingsPath = launchedArgs[settingsIndex + 1];
|
||||
expect(launchSettingsPath).toBeDefined();
|
||||
expect(launchSettingsPath).not.toBe(settingsPath);
|
||||
|
||||
const persistedLaunchSettings = JSON.parse(
|
||||
fs.readFileSync(claudeSettingsSnapshotPath, 'utf8')
|
||||
) as {
|
||||
env?: Record<string, string>;
|
||||
hooks?: {
|
||||
PreToolUse?: Array<{
|
||||
matcher?: string;
|
||||
hooks?: Array<{ command?: string }>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_BASE_URL).toBeUndefined();
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_API_KEY).toBeUndefined();
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_MODEL).toBe('gpt-5.4');
|
||||
expect(
|
||||
persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command
|
||||
).toBe('echo keep-profile-settings');
|
||||
|
||||
const launchedEnv = fs.readFileSync(claudeEnvLogPath, 'utf8');
|
||||
expect(launchedEnv).toContain('stripAnthropic=');
|
||||
expect(launchedEnv).toContain('anthropicBaseUrl=http://127.0.0.1:');
|
||||
expect(launchedEnv).not.toContain('anthropicBaseUrl=https://api.openai.com/v1');
|
||||
expect(launchedEnv).toContain('anthropicModel=gpt-5.4');
|
||||
expect(launchedEnv).toContain('maxOutputTokens=12345');
|
||||
expect(fs.readFileSync(claudeSettingsPathLogPath, 'utf8')).toBe(launchSettingsPath);
|
||||
expect(fs.existsSync(launchSettingsPath as string)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not auto-enable browser reuse for settings-profile launches from env overrides alone', async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ const STEERING_PROMPT_SNIPPET =
|
||||
'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches';
|
||||
const spawnCalls: SpawnCall[] = [];
|
||||
const spawnSyncCalls: SpawnSyncCall[] = [];
|
||||
const launchSettingsSnapshots: Array<{ path: string; content: string }> = [];
|
||||
const originalPlatform = process.platform;
|
||||
let baselineSigintListeners: Array<(...args: unknown[]) => void> = [];
|
||||
let baselineSigtermListeners: Array<(...args: unknown[]) => void> = [];
|
||||
@@ -99,6 +100,16 @@ function registerChildProcessMock(): void {
|
||||
}
|
||||
|
||||
spawnCalls.push({ command, args, options });
|
||||
const settingsIndex = args.indexOf('--settings');
|
||||
if (settingsIndex >= 0) {
|
||||
const settingsPath = args[settingsIndex + 1];
|
||||
if (settingsPath && fs.existsSync(settingsPath)) {
|
||||
launchSettingsSnapshots.push({
|
||||
path: settingsPath,
|
||||
content: fs.readFileSync(settingsPath, 'utf8'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const child = createMockChild();
|
||||
setTimeout(() => child.emit('close', 0), 0);
|
||||
@@ -193,6 +204,7 @@ describe('CLAUDECODE environment stripping', () => {
|
||||
beforeEach(() => {
|
||||
spawnCalls.length = 0;
|
||||
spawnSyncCalls.length = 0;
|
||||
launchSettingsSnapshots.length = 0;
|
||||
process.env.CCS_QUIET = '1';
|
||||
|
||||
// Save original env values for restoration in afterEach
|
||||
@@ -651,6 +663,14 @@ describe('CLAUDECODE environment stripping', () => {
|
||||
ANTHROPIC_MODEL: 'gpt-5.4',
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS: '12345',
|
||||
},
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: 'Read',
|
||||
hooks: [{ type: 'command', command: 'echo headless-bridge-hook' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
@@ -679,6 +699,37 @@ describe('CLAUDECODE environment stripping', () => {
|
||||
expect(env.ANTHROPIC_AUTH_TOKEN).not.toBe('parent-routing-token');
|
||||
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.4');
|
||||
expect(env.CLAUDE_CODE_MAX_OUTPUT_TOKENS).toBe('12345');
|
||||
|
||||
const args = spawnCalls[0].args;
|
||||
const settingsIndex = args.indexOf('--settings');
|
||||
expect(settingsIndex).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const launchSettingsPath = args[settingsIndex + 1];
|
||||
expect(launchSettingsPath).toBeDefined();
|
||||
expect(launchSettingsPath).not.toBe(path.join(ccsDir, 'bridge.settings.json'));
|
||||
const launchSettingsSnapshot = launchSettingsSnapshots.find(
|
||||
(snapshot) => snapshot.path === launchSettingsPath
|
||||
);
|
||||
expect(launchSettingsSnapshot).toBeDefined();
|
||||
|
||||
const persistedLaunchSettings = JSON.parse(launchSettingsSnapshot?.content || '{}') as {
|
||||
env?: Record<string, string>;
|
||||
hooks?: {
|
||||
PreToolUse?: Array<{
|
||||
matcher?: string;
|
||||
hooks?: Array<{ command?: string }>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_BASE_URL).toBeUndefined();
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_API_KEY).toBeUndefined();
|
||||
expect(persistedLaunchSettings.env?.ANTHROPIC_MODEL).toBe('gpt-5.4');
|
||||
expect(
|
||||
persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command
|
||||
).toBe('echo headless-bridge-hook');
|
||||
expect(fs.existsSync(launchSettingsPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('headless executor prepares image-analysis MCP and suppresses the legacy hook on healthy launches', async () => {
|
||||
|
||||
Reference in New Issue
Block a user