mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
fix(cliproxy): preserve image-analysis runtime readiness
This commit is contained in:
@@ -20,11 +20,15 @@ import { CLIProxyProvider } from '../types';
|
||||
import { CompositeTierConfig } from '../../config/unified-config-types';
|
||||
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
|
||||
import { getImageAnalysisHookEnv } from '../../utils/hooks/get-image-analysis-hook-env';
|
||||
import { resolveImageAnalysisRuntimeStatus } from '../../utils/hooks/image-analysis-runtime-status';
|
||||
import { hasImageAnalysisProfileHook } from '../../utils/hooks/image-analyzer-profile-hook-injector';
|
||||
import { hasImageAnalyzerHook } from '../../utils/hooks/image-analyzer-hook-installer';
|
||||
import { stripClaudeCodeEnv } from '../../utils/shell-executor';
|
||||
import { CodexReasoningProxy } from '../codex-reasoning-proxy';
|
||||
import { ToolSanitizationProxy } from '../tool-sanitization-proxy';
|
||||
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
|
||||
import { MODEL_ENV_VAR_KEYS, normalizeModelIdForProvider } from '../model-id-normalizer';
|
||||
import type { ProxyTarget } from '../proxy-target-resolver';
|
||||
|
||||
export interface RemoteProxyConfig {
|
||||
host: string;
|
||||
@@ -61,8 +65,38 @@ export interface ProxyChainConfig {
|
||||
compositeDefaultTier?: 'opus' | 'sonnet' | 'haiku';
|
||||
/** Optional inherited continuity directory from mapped account profile */
|
||||
claudeConfigDir?: string;
|
||||
/** Execution-aware image analysis env prepared by the caller */
|
||||
imageAnalysisEnv?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CliproxyImageAnalysisDeps {
|
||||
getImageAnalysisHookEnv: typeof getImageAnalysisHookEnv;
|
||||
hasImageAnalysisProfileHook: typeof hasImageAnalysisProfileHook;
|
||||
hasImageAnalyzerHook: typeof hasImageAnalyzerHook;
|
||||
resolveImageAnalysisRuntimeStatus: typeof resolveImageAnalysisRuntimeStatus;
|
||||
}
|
||||
|
||||
interface ResolveCliproxyImageAnalysisEnvOptions {
|
||||
profileName: string;
|
||||
provider: CLIProxyProvider;
|
||||
profileSettingsPath?: string;
|
||||
isComposite?: boolean;
|
||||
proxyTarget: ProxyTarget;
|
||||
proxyReachable: boolean;
|
||||
}
|
||||
|
||||
export interface CliproxyImageAnalysisResolution {
|
||||
env: Record<string, string>;
|
||||
warning: string | null;
|
||||
}
|
||||
|
||||
const defaultCliproxyImageAnalysisDeps: CliproxyImageAnalysisDeps = {
|
||||
getImageAnalysisHookEnv,
|
||||
hasImageAnalysisProfileHook,
|
||||
hasImageAnalyzerHook,
|
||||
resolveImageAnalysisRuntimeStatus,
|
||||
};
|
||||
|
||||
const CODEX_EFFORT_SUFFIX_REGEX = /^(.*)-(xhigh|high|medium)$/i;
|
||||
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
|
||||
|
||||
@@ -95,6 +129,49 @@ function normalizeCodexEnvForDirectUpstream(envVars: NodeJS.ProcessEnv): NodeJS.
|
||||
return nextEnv ?? envVars;
|
||||
}
|
||||
|
||||
export async function resolveCliproxyImageAnalysisEnv(
|
||||
options: ResolveCliproxyImageAnalysisEnvOptions,
|
||||
deps: Partial<CliproxyImageAnalysisDeps> = {}
|
||||
): Promise<CliproxyImageAnalysisResolution> {
|
||||
const resolvedDeps = { ...defaultCliproxyImageAnalysisDeps, ...deps };
|
||||
const context = {
|
||||
profileName: options.profileName,
|
||||
profileType: 'cliproxy' as const,
|
||||
cliproxyProvider: options.provider,
|
||||
isComposite: options.isComposite,
|
||||
settingsPath: options.profileSettingsPath,
|
||||
hookInstalled: resolvedDeps.hasImageAnalysisProfileHook(
|
||||
options.profileName,
|
||||
options.profileSettingsPath
|
||||
),
|
||||
sharedHookInstalled: resolvedDeps.hasImageAnalyzerHook(),
|
||||
};
|
||||
|
||||
const env = resolvedDeps.getImageAnalysisHookEnv(context);
|
||||
const currentProvider = env['CCS_CURRENT_PROVIDER'];
|
||||
if (env['CCS_IMAGE_ANALYSIS_SKIP'] === '1' || !currentProvider) {
|
||||
return { env, warning: null };
|
||||
}
|
||||
|
||||
const status = await resolvedDeps.resolveImageAnalysisRuntimeStatus(context, undefined, {
|
||||
getProxyTarget: () => options.proxyTarget,
|
||||
isCliproxyRunning: async () => options.proxyReachable,
|
||||
});
|
||||
|
||||
if (status.effectiveRuntimeMode === 'native-read') {
|
||||
return {
|
||||
env: {
|
||||
...env,
|
||||
CCS_CURRENT_PROVIDER: '',
|
||||
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
||||
},
|
||||
warning: `${status.effectiveRuntimeReason || `Image analysis via ${currentProvider} is unavailable.`} This session will use native Read.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { env, warning: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build final environment variables for Claude CLI execution
|
||||
* Handles proxy chain ordering and integration with hooks
|
||||
@@ -116,6 +193,7 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
|
||||
compositeTiers,
|
||||
compositeDefaultTier,
|
||||
claudeConfigDir,
|
||||
imageAnalysisEnv: resolvedImageAnalysisEnv,
|
||||
} = config;
|
||||
|
||||
// Build base env vars - check remote mode first
|
||||
@@ -253,7 +331,7 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
|
||||
|
||||
// Add hook environment variables
|
||||
const webSearchEnv = getWebSearchHookEnv();
|
||||
const imageAnalysisEnv = getImageAnalysisHookEnv(provider);
|
||||
const imageAnalysisEnv = resolvedImageAnalysisEnv ?? getImageAnalysisHookEnv(provider);
|
||||
|
||||
// Merge all environment variables (filter undefined values)
|
||||
const baseEnv = Object.fromEntries(
|
||||
|
||||
@@ -66,7 +66,11 @@ import { resolveProfileContinuityInheritance } from '../../auth/profile-continui
|
||||
|
||||
// Import modular components
|
||||
import { waitForProxyReadyWithSpinner, spawnProxy } from './lifecycle-manager';
|
||||
import { buildClaudeEnvironment, logEnvironment } from './env-resolver';
|
||||
import {
|
||||
buildClaudeEnvironment,
|
||||
logEnvironment,
|
||||
resolveCliproxyImageAnalysisEnv,
|
||||
} from './env-resolver';
|
||||
import {
|
||||
isNetworkError,
|
||||
handleNetworkError,
|
||||
@@ -172,6 +176,7 @@ export async function execClaudeWithCLIProxy(
|
||||
port: cliproxyServerConfig.remote.port,
|
||||
protocol: cliproxyServerConfig.remote.protocol,
|
||||
auth_token: cliproxyServerConfig.remote.auth_token,
|
||||
management_key: cliproxyServerConfig.remote.management_key,
|
||||
timeout: cliproxyServerConfig.remote.timeout,
|
||||
}
|
||||
: undefined,
|
||||
@@ -819,6 +824,33 @@ export async function execClaudeWithCLIProxy(
|
||||
}
|
||||
}
|
||||
|
||||
const imageAnalysisProxyTarget =
|
||||
useRemoteProxy && proxyConfig.host
|
||||
? {
|
||||
host: proxyConfig.host,
|
||||
port: proxyConfig.port,
|
||||
protocol: proxyConfig.protocol,
|
||||
authToken: proxyConfig.authToken,
|
||||
managementKey: proxyConfig.managementKey,
|
||||
allowSelfSigned: proxyConfig.allowSelfSigned,
|
||||
isRemote: true as const,
|
||||
}
|
||||
: {
|
||||
host: '127.0.0.1',
|
||||
port: cfg.port,
|
||||
protocol: 'http' as const,
|
||||
isRemote: false as const,
|
||||
};
|
||||
const { env: imageAnalysisEnv, warning: imageAnalysisWarning } =
|
||||
await resolveCliproxyImageAnalysisEnv({
|
||||
profileName: cfg.profileName || provider,
|
||||
provider,
|
||||
profileSettingsPath: cfg.customSettingsPath,
|
||||
isComposite: cfg.isComposite,
|
||||
proxyTarget: imageAnalysisProxyTarget,
|
||||
proxyReachable: true,
|
||||
});
|
||||
|
||||
// 9. Setup tool sanitization proxy
|
||||
let toolSanitizationProxy: ToolSanitizationProxy | null = null;
|
||||
let toolSanitizationPort: number | null = null;
|
||||
@@ -861,6 +893,7 @@ export async function execClaudeWithCLIProxy(
|
||||
compositeTiers: cfg.compositeTiers,
|
||||
compositeDefaultTier: cfg.compositeDefaultTier,
|
||||
claudeConfigDir: inheritedClaudeConfigDir,
|
||||
imageAnalysisEnv,
|
||||
});
|
||||
|
||||
if (initialEnvVars.ANTHROPIC_BASE_URL) {
|
||||
@@ -957,6 +990,7 @@ export async function execClaudeWithCLIProxy(
|
||||
compositeTiers: cfg.compositeTiers,
|
||||
compositeDefaultTier: cfg.compositeDefaultTier,
|
||||
claudeConfigDir: inheritedClaudeConfigDir,
|
||||
imageAnalysisEnv,
|
||||
});
|
||||
|
||||
if (cfg.isComposite && cfg.compositeTiers && cfg.compositeDefaultTier) {
|
||||
@@ -973,6 +1007,9 @@ export async function execClaudeWithCLIProxy(
|
||||
|
||||
const webSearchEnv = getWebSearchHookEnv();
|
||||
logEnvironment(env, webSearchEnv, verbose);
|
||||
if (imageAnalysisWarning) {
|
||||
console.error(info(imageAnalysisWarning));
|
||||
}
|
||||
|
||||
// 11b. Print thinking status feedback (TTY only, non-piped sessions)
|
||||
if (process.stderr.isTTY) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Supports both local (spawn CLIProxyAPI) and remote (connect to external) modes.
|
||||
*/
|
||||
|
||||
import { ResolvedProxyConfig } from './types';
|
||||
import type { ResolvedProxyConfig } from './types';
|
||||
import { CLIPROXY_DEFAULT_PORT, validatePort } from './config-generator';
|
||||
|
||||
/** CLI flags for proxy configuration */
|
||||
@@ -233,6 +233,7 @@ export function resolveProxyConfig(
|
||||
port?: number;
|
||||
protocol?: 'http' | 'https';
|
||||
auth_token?: string;
|
||||
management_key?: string;
|
||||
timeout?: number;
|
||||
fallback_enabled?: boolean;
|
||||
};
|
||||
@@ -290,6 +291,7 @@ export function resolveProxyConfig(
|
||||
|
||||
// Merge auth token: CLI > ENV > config.yaml
|
||||
resolved.authToken = cliFlags.authToken ?? envConfig.authToken ?? yamlConfig.remote?.auth_token;
|
||||
resolved.managementKey = yamlConfig.remote?.management_key;
|
||||
|
||||
// Merge timeout: CLI > ENV > config.yaml > default (2000ms in executor)
|
||||
resolved.timeout = cliFlags.timeout ?? envConfig.timeout ?? yamlConfig.remote?.timeout;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
normalizeProtocol,
|
||||
validateRemotePort,
|
||||
} from './config-generator';
|
||||
import { getProxyEnvVars } from './proxy-config-resolver';
|
||||
import { getEffectiveManagementSecret } from './auth-token-manager';
|
||||
|
||||
/** Resolved proxy target for making requests */
|
||||
@@ -27,6 +28,8 @@ export interface ProxyTarget {
|
||||
authToken?: string;
|
||||
/** Optional management key for management API endpoints (/v0/management/*) */
|
||||
managementKey?: string;
|
||||
/** Whether HTTPS requests should allow self-signed certificates */
|
||||
allowSelfSigned?: boolean;
|
||||
/** True if targeting remote server, false if local */
|
||||
isRemote: boolean;
|
||||
}
|
||||
@@ -46,6 +49,7 @@ function loadCliproxyServerConfig(): CliproxyServerConfig | undefined {
|
||||
*/
|
||||
export function getProxyTarget(): ProxyTarget {
|
||||
const config = loadCliproxyServerConfig();
|
||||
const envConfig = getProxyEnvVars();
|
||||
|
||||
if (config?.remote?.enabled && config.remote?.host) {
|
||||
// Normalize protocol (handles case sensitivity and invalid values)
|
||||
@@ -60,6 +64,7 @@ export function getProxyTarget(): ProxyTarget {
|
||||
protocol,
|
||||
authToken: config.remote.auth_token || undefined, // Empty string -> undefined
|
||||
managementKey: config.remote.management_key || undefined, // Empty string -> undefined
|
||||
allowSelfSigned: envConfig.allowSelfSigned,
|
||||
isRemote: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Fetches and transforms auth data from remote CLIProxyAPI.
|
||||
*/
|
||||
|
||||
import * as https from 'https';
|
||||
import {
|
||||
getProxyTarget,
|
||||
buildProxyUrl,
|
||||
@@ -15,6 +16,87 @@ import type { CLIProxyProvider } from './types';
|
||||
/** Timeout for remote fetch requests (ms) */
|
||||
const REMOTE_FETCH_TIMEOUT_MS = 5000;
|
||||
|
||||
async function fetchRemoteAuthResponse(
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
target: ProxyTarget
|
||||
): Promise<Response> {
|
||||
if (target.protocol !== 'https' || !target.allowSelfSigned) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REMOTE_FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
return await fetch(url, {
|
||||
signal: controller.signal,
|
||||
headers,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
let settled = false;
|
||||
|
||||
const settle = (callback: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
callback();
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
const timeoutError = new Error('Request timeout');
|
||||
req.destroy(timeoutError);
|
||||
settle(() => reject(timeoutError));
|
||||
}, REMOTE_FETCH_TIMEOUT_MS);
|
||||
|
||||
const req = https.request(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
agent,
|
||||
timeout: REMOTE_FETCH_TIMEOUT_MS,
|
||||
},
|
||||
(res) => {
|
||||
let body = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
settle(() =>
|
||||
resolve(
|
||||
new Response(body, {
|
||||
status: res.statusCode || 500,
|
||||
statusText: res.statusMessage ?? '',
|
||||
headers:
|
||||
typeof res.headers['content-type'] === 'string'
|
||||
? { 'Content-Type': res.headers['content-type'] }
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.on('error', (error) => {
|
||||
settle(() => reject(error));
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
const timeoutError = new Error('Request timeout');
|
||||
req.destroy(timeoutError);
|
||||
settle(() => reject(timeoutError));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/** Remote auth file from CLIProxyAPI /v0/management/auth-files */
|
||||
interface RemoteAuthFile {
|
||||
id: string;
|
||||
@@ -60,16 +142,8 @@ export async function fetchRemoteAuthStatus(target?: ProxyTarget): Promise<Remot
|
||||
const url = buildProxyUrl(proxyTarget, '/v0/management/auth-files');
|
||||
const headers = buildManagementHeaders(proxyTarget);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REMOTE_FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
const response = await fetchRemoteAuthResponse(url, headers, proxyTarget);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
@@ -87,11 +161,12 @@ export async function fetchRemoteAuthStatus(target?: ProxyTarget): Promise<Remot
|
||||
|
||||
return transformRemoteAuthFiles(data.files as RemoteAuthFile[]);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error('Remote proxy connection timed out');
|
||||
}
|
||||
if (error instanceof Error && error.message === 'Request timeout') {
|
||||
throw new Error('Remote proxy connection timed out');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +268,8 @@ export interface ResolvedProxyConfig {
|
||||
protocol: 'http' | 'https';
|
||||
/** Auth token for remote proxy authentication */
|
||||
authToken?: string;
|
||||
/** Management key for remote management endpoints */
|
||||
managementKey?: string;
|
||||
/** Enable fallback to local when remote unreachable (default: true) */
|
||||
fallbackEnabled: boolean;
|
||||
/** Auto-start local proxy if not running (default: true) */
|
||||
|
||||
@@ -2,7 +2,11 @@ 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 } from '../../../src/cliproxy/executor/env-resolver';
|
||||
import {
|
||||
buildClaudeEnvironment,
|
||||
resolveCliproxyImageAnalysisEnv,
|
||||
} from '../../../src/cliproxy/executor/env-resolver';
|
||||
import type { ImageAnalysisStatus } from '../../../src/utils/hooks';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -37,6 +41,37 @@ function createCodexSettingsFile(models: {
|
||||
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) {
|
||||
@@ -112,4 +147,117 @@ describe('buildClaudeEnvironment codex fallback normalization', () => {
|
||||
|
||||
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.warning).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,6 +282,18 @@ describe('proxy-config-resolver', () => {
|
||||
expect(config.host).toBe('yaml-host.example.com');
|
||||
});
|
||||
|
||||
it('should preserve YAML management key for remote mode', () => {
|
||||
const { config } = resolveProxyConfig([], {
|
||||
remote: {
|
||||
host: 'yaml-host.example.com',
|
||||
auth_token: 'remote-auth-token',
|
||||
management_key: 'remote-management-key',
|
||||
},
|
||||
});
|
||||
expect(config.mode).toBe('remote');
|
||||
expect(config.managementKey).toBe('remote-management-key');
|
||||
});
|
||||
|
||||
it('should allow CLI --proxy-host to override YAML enabled:false', () => {
|
||||
const { config } = resolveProxyConfig(['--proxy-host', 'cli-host'], {
|
||||
remote: { enabled: false, host: 'yaml-host' },
|
||||
|
||||
Reference in New Issue
Block a user