mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { afterEach, describe, expect, it } from 'bun:test';
|
|
import {
|
|
buildClaudeEnvironment,
|
|
resolveCliproxyImageAnalysisEnv,
|
|
} from '../../../src/cliproxy/executor/env-resolver';
|
|
import type { ImageAnalysisStatus } from '../../../src/utils/hooks';
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function createCodexSettingsFile(models: {
|
|
defaultModel: string;
|
|
opusModel: string;
|
|
sonnetModel: string;
|
|
haikuModel: string;
|
|
}): string {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-fallback-'));
|
|
tempDirs.push(tempDir);
|
|
|
|
const settingsPath = path.join(tempDir, 'codex-test.settings.json');
|
|
fs.writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify(
|
|
{
|
|
env: {
|
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
|
|
ANTHROPIC_AUTH_TOKEN: 'test-token',
|
|
ANTHROPIC_MODEL: models.defaultModel,
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opusModel,
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnetModel,
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haikuModel,
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
) + '\n'
|
|
);
|
|
|
|
return settingsPath;
|
|
}
|
|
|
|
function createImageAnalysisStatus(
|
|
overrides: Partial<ImageAnalysisStatus> = {}
|
|
): ImageAnalysisStatus {
|
|
return {
|
|
enabled: true,
|
|
supported: true,
|
|
status: 'active',
|
|
backendId: 'agy',
|
|
backendDisplayName: 'Antigravity',
|
|
model: 'gemini-2.5-pro',
|
|
resolutionSource: 'cliproxy-provider',
|
|
reason: null,
|
|
shouldPersistHook: true,
|
|
persistencePath: '/tmp/orq.settings.json',
|
|
runtimePath: '/api/provider/agy',
|
|
usesCurrentTarget: true,
|
|
usesCurrentAuthToken: true,
|
|
hookInstalled: true,
|
|
sharedHookInstalled: true,
|
|
authReadiness: 'ready',
|
|
authProvider: 'agy',
|
|
authDisplayName: 'Antigravity',
|
|
authReason: null,
|
|
proxyReadiness: 'ready',
|
|
proxyReason: 'Local CLIProxy service is reachable.',
|
|
effectiveRuntimeMode: 'cliproxy-image-analysis',
|
|
effectiveRuntimeReason: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('buildClaudeEnvironment codex fallback normalization', () => {
|
|
afterEach(() => {
|
|
while (tempDirs.length > 0) {
|
|
const tempDir = tempDirs.pop();
|
|
if (tempDir) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
it('normalizes codex effort aliases when reasoning proxy is unavailable', () => {
|
|
const settingsPath = createCodexSettingsFile({
|
|
defaultModel: 'gpt-5.3-codex-high',
|
|
opusModel: 'gpt-5.3-codex-xhigh',
|
|
sonnetModel: 'gpt-5.3-codex-high',
|
|
haikuModel: 'gpt-5.4-mini-medium',
|
|
});
|
|
|
|
const env = buildClaudeEnvironment({
|
|
provider: 'codex',
|
|
useRemoteProxy: false,
|
|
localPort: 8317,
|
|
customSettingsPath: settingsPath,
|
|
verbose: false,
|
|
});
|
|
|
|
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex(high)');
|
|
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex(xhigh)');
|
|
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex(high)');
|
|
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini(medium)');
|
|
});
|
|
|
|
it('keeps codex effort aliases when reasoning proxy is active', () => {
|
|
const settingsPath = createCodexSettingsFile({
|
|
defaultModel: 'gpt-5.3-codex-high',
|
|
opusModel: 'gpt-5.3-codex-xhigh',
|
|
sonnetModel: 'gpt-5.3-codex-high',
|
|
haikuModel: 'gpt-5.4-mini-medium',
|
|
});
|
|
|
|
const env = buildClaudeEnvironment({
|
|
provider: 'codex',
|
|
useRemoteProxy: false,
|
|
localPort: 8317,
|
|
customSettingsPath: settingsPath,
|
|
codexReasoningPort: 9444,
|
|
verbose: false,
|
|
});
|
|
|
|
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-high');
|
|
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh');
|
|
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high');
|
|
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini-medium');
|
|
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:9444/api/provider/codex');
|
|
});
|
|
|
|
it('propagates inherited CLAUDE_CONFIG_DIR when provided', () => {
|
|
const settingsPath = createCodexSettingsFile({
|
|
defaultModel: 'gpt-5.3-codex',
|
|
opusModel: 'gpt-5.3-codex',
|
|
sonnetModel: 'gpt-5.3-codex',
|
|
haikuModel: 'gpt-5-mini',
|
|
});
|
|
|
|
const env = buildClaudeEnvironment({
|
|
provider: 'codex',
|
|
useRemoteProxy: false,
|
|
localPort: 8317,
|
|
customSettingsPath: settingsPath,
|
|
claudeConfigDir: '/tmp/.ccs/instances/pro',
|
|
verbose: false,
|
|
});
|
|
|
|
expect(env.CLAUDE_CONFIG_DIR).toBe('/tmp/.ccs/instances/pro');
|
|
});
|
|
|
|
it('uses an execution-aware image analysis env override when provided', () => {
|
|
const env = buildClaudeEnvironment({
|
|
provider: 'agy',
|
|
useRemoteProxy: false,
|
|
localPort: 8317,
|
|
verbose: false,
|
|
imageAnalysisEnv: {
|
|
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
|
CCS_IMAGE_ANALYSIS_TIMEOUT: '60',
|
|
CCS_IMAGE_ANALYSIS_PROVIDER_MODELS: 'agy:gemini-2.5-pro',
|
|
CCS_CURRENT_PROVIDER: '',
|
|
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
|
},
|
|
});
|
|
|
|
expect(env.CCS_CURRENT_PROVIDER).toBe('');
|
|
expect(env.CCS_IMAGE_ANALYSIS_SKIP).toBe('1');
|
|
});
|
|
});
|
|
|
|
describe('resolveCliproxyImageAnalysisEnv', () => {
|
|
it('falls back to native read when runtime status is not launchable', async () => {
|
|
const result = await resolveCliproxyImageAnalysisEnv(
|
|
{
|
|
profileName: 'orq',
|
|
provider: 'agy',
|
|
profileSettingsPath: '/tmp/orq.settings.json',
|
|
proxyTarget: {
|
|
host: '127.0.0.1',
|
|
port: 8317,
|
|
protocol: 'http',
|
|
isRemote: false,
|
|
},
|
|
proxyReachable: true,
|
|
},
|
|
{
|
|
getImageAnalysisHookEnv: () => ({
|
|
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
|
CCS_IMAGE_ANALYSIS_TIMEOUT: '60',
|
|
CCS_IMAGE_ANALYSIS_PROVIDER_MODELS: 'agy:gemini-2.5-pro',
|
|
CCS_CURRENT_PROVIDER: 'agy',
|
|
CCS_IMAGE_ANALYSIS_SKIP: '0',
|
|
}),
|
|
hasImageAnalysisProfileHook: () => true,
|
|
hasImageAnalyzerHook: () => true,
|
|
resolveImageAnalysisRuntimeStatus: async (context, _config, deps) => {
|
|
expect(context.profileName).toBe('orq');
|
|
expect(context.cliproxyProvider).toBe('agy');
|
|
expect(context.hookInstalled).toBe(true);
|
|
expect(context.sharedHookInstalled).toBe(true);
|
|
expect(deps?.getProxyTarget?.().isRemote).toBe(false);
|
|
return createImageAnalysisStatus({
|
|
authReadiness: 'missing',
|
|
authReason: 'Antigravity auth is missing.',
|
|
effectiveRuntimeMode: 'native-read',
|
|
effectiveRuntimeReason: 'Antigravity auth is missing.',
|
|
});
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(result.env.CCS_CURRENT_PROVIDER).toBe('');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_SKIP).toBe('1');
|
|
expect(result.warning).toContain('Antigravity auth is missing.');
|
|
expect(result.warning).toContain('native Read');
|
|
});
|
|
|
|
it('keeps cliproxy image analysis active when the execution target is reachable', async () => {
|
|
const result = await resolveCliproxyImageAnalysisEnv(
|
|
{
|
|
profileName: 'orq',
|
|
provider: 'agy',
|
|
isComposite: true,
|
|
proxyTarget: {
|
|
host: 'remote.example.com',
|
|
port: 9443,
|
|
protocol: 'https',
|
|
authToken: 'remote-token',
|
|
managementKey: 'remote-management-key',
|
|
allowSelfSigned: true,
|
|
isRemote: true,
|
|
},
|
|
proxyReachable: true,
|
|
},
|
|
{
|
|
getImageAnalysisHookEnv: () => ({
|
|
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
|
CCS_IMAGE_ANALYSIS_TIMEOUT: '60',
|
|
CCS_IMAGE_ANALYSIS_PROVIDER_MODELS: 'agy:gemini-2.5-pro',
|
|
CCS_CURRENT_PROVIDER: 'agy',
|
|
CCS_IMAGE_ANALYSIS_SKIP: '0',
|
|
}),
|
|
hasImageAnalysisProfileHook: () => true,
|
|
hasImageAnalyzerHook: () => true,
|
|
resolveImageAnalysisRuntimeStatus: async (context, _config, deps) => {
|
|
expect(context.isComposite).toBe(true);
|
|
expect(deps?.getProxyTarget?.().host).toBe('remote.example.com');
|
|
expect(deps?.getProxyTarget?.().managementKey).toBe('remote-management-key');
|
|
expect(deps?.getProxyTarget?.().allowSelfSigned).toBe(true);
|
|
return createImageAnalysisStatus({
|
|
resolutionSource: 'cliproxy-composite',
|
|
proxyReadiness: 'remote',
|
|
proxyReason: 'Remote CLIProxy target remote.example.com:9443 is reachable.',
|
|
});
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(result.env.CCS_CURRENT_PROVIDER).toBe('agy');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_SKIP).toBe('0');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL).toBe('https://remote.example.com:9443');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_PATH).toBe('/api/provider/agy');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY).toBe('remote-token');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED).toBe('1');
|
|
expect(result.warning).toBeNull();
|
|
});
|
|
|
|
it('pins local cliproxy image analysis to the resolved local API key', async () => {
|
|
const result = await resolveCliproxyImageAnalysisEnv(
|
|
{
|
|
profileName: 'orq',
|
|
provider: 'agy',
|
|
profileSettingsPath: '/tmp/orq.settings.json',
|
|
proxyTarget: {
|
|
host: '127.0.0.1',
|
|
port: 8317,
|
|
protocol: 'http',
|
|
isRemote: false,
|
|
},
|
|
proxyReachable: true,
|
|
},
|
|
{
|
|
getImageAnalysisHookEnv: () => ({
|
|
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
|
CCS_IMAGE_ANALYSIS_TIMEOUT: '60',
|
|
CCS_IMAGE_ANALYSIS_PROVIDER_MODELS: 'agy:gemini-2.5-pro',
|
|
CCS_CURRENT_PROVIDER: 'agy',
|
|
CCS_IMAGE_ANALYSIS_SKIP: '0',
|
|
}),
|
|
hasImageAnalysisProfileHook: () => true,
|
|
hasImageAnalyzerHook: () => true,
|
|
resolveImageAnalysisRuntimeStatus: async () => createImageAnalysisStatus(),
|
|
getLocalRuntimeApiKey: () => 'local-runtime-token',
|
|
}
|
|
);
|
|
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL).toBe('http://127.0.0.1:8317');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_PATH).toBe('/api/provider/agy');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY).toBe('local-runtime-token');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED).toBe('0');
|
|
expect(result.warning).toBeNull();
|
|
});
|
|
|
|
it('routes remote HTTPS image analysis through the local tunnel when available', async () => {
|
|
const result = await resolveCliproxyImageAnalysisEnv(
|
|
{
|
|
profileName: 'orq',
|
|
provider: 'agy',
|
|
profileSettingsPath: '/tmp/orq.settings.json',
|
|
proxyTarget: {
|
|
host: 'remote.example.com',
|
|
port: 9443,
|
|
protocol: 'https',
|
|
authToken: 'remote-token',
|
|
managementKey: 'remote-management-key',
|
|
allowSelfSigned: true,
|
|
isRemote: true,
|
|
},
|
|
tunnelPort: 9911,
|
|
proxyReachable: true,
|
|
},
|
|
{
|
|
getImageAnalysisHookEnv: () => ({
|
|
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
|
CCS_IMAGE_ANALYSIS_TIMEOUT: '60',
|
|
CCS_IMAGE_ANALYSIS_PROVIDER_MODELS: 'agy:gemini-2.5-pro',
|
|
CCS_CURRENT_PROVIDER: 'agy',
|
|
CCS_IMAGE_ANALYSIS_SKIP: '0',
|
|
}),
|
|
hasImageAnalysisProfileHook: () => true,
|
|
hasImageAnalyzerHook: () => true,
|
|
resolveImageAnalysisRuntimeStatus: async () => createImageAnalysisStatus(),
|
|
}
|
|
);
|
|
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL).toBe('http://127.0.0.1:9911');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY).toBe('remote-token');
|
|
expect(result.env.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED).toBe('0');
|
|
expect(result.warning).toBeNull();
|
|
});
|
|
});
|