Files
ccs/tests/unit/utils/hooks/image-analysis-runtime-status-circular-dependency.test.ts
T
Aaron VuandClaude fe3447f487 fix(image-analysis): honor configured profile_backends at launch
resolveImageAnalysisRuntimeStatus defaulted to DEFAULT_IMAGE_ANALYSIS_CONFIG
when callers omitted the config argument. That constant ships empty
profile_backends and a gemini fallback_backend, so the launch paths that call
it without a config (settings profile dispatch and headless delegation) never
saw user-configured mappings.

A profile mapped to another backend still resolved to gemini, failed the
Gemini auth check, and silently dropped to native Read. Profiles whose model
has no vision support could not read images at all, even with a reachable
CLIProxy and an authenticated backend.

getImageAnalysisHookEnv already reads the saved config, so the launch env and
the runtime status disagreed on the same launch: CCS_IMAGE_ANALYSIS_BACKEND_ID
carried the mapped backend while the status object reported native-read.

Default to getImageAnalysisConfig() so both read the same source. Callers that
pass an explicit config keep their existing behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 15:27:02 +07:00

152 lines
4.9 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { readFileSync } from 'fs';
import { dirname } from 'path';
import type { ImageAnalysisStatus } from '../../../../src/utils/hooks/image-analysis-backend-resolver';
import type { AuthStatus } from '../../../../src/cliproxy/auth/auth-handler';
import {
ModuleKind,
ScriptTarget,
transpileModule,
type CompilerOptions,
} from 'typescript';
function createStatus(overrides: Partial<ImageAnalysisStatus> = {}): ImageAnalysisStatus {
return {
enabled: true,
supported: true,
status: 'active',
backendId: 'ghcp',
backendDisplayName: 'GitHub Copilot (OAuth)',
model: 'claude-haiku-4.5',
resolutionSource: 'profile-backend',
reason: null,
shouldPersistHook: true,
persistencePath: '/tmp/orq.settings.json',
runtimePath: '/api/provider/ghcp',
usesCurrentTarget: true,
usesCurrentAuthToken: true,
hookInstalled: true,
sharedHookInstalled: true,
authReadiness: 'unknown',
authProvider: 'ghcp',
authDisplayName: 'GitHub Copilot (OAuth)',
authReason: 'Auth readiness has not been verified yet.',
proxyReadiness: 'unknown',
proxyReason: 'CLIProxy runtime readiness has not been verified yet.',
effectiveRuntimeMode: 'native-read',
effectiveRuntimeReason: null,
profileModel: 'claude-haiku-4.5',
nativeReadPreference: false,
nativeImageCapable: true,
nativeImageReason: 'claude-haiku-4.5 can read images natively.',
...overrides,
};
}
describe('image-analysis-runtime-status circular dependency regression', () => {
it('reads auth deps from the CommonJS module object at call time', async () => {
const authHandlerModule: {
initializeAccounts: undefined | (() => void);
getAuthStatus: undefined | ((provider: 'ghcp') => AuthStatus);
} = {
initializeAccounts: undefined,
getAuthStatus: undefined,
};
const sourcePath = new URL(
'../../../../src/utils/hooks/image-analysis-runtime-status.ts',
import.meta.url
);
const source = readFileSync(sourcePath, 'utf8');
const compilerOptions: CompilerOptions = {
module: ModuleKind.CommonJS,
target: ScriptTarget.ES2020,
esModuleInterop: true,
};
const transpiled = transpileModule(source, { compilerOptions }).outputText;
const module = { exports: {} as Record<string, unknown> };
const requireMap: Record<string, unknown> = {
'../../cliproxy/auth/auth-handler': authHandlerModule,
'../../cliproxy/services/remote-proxy-client': {
checkRemoteProxy: async () => ({ reachable: false, error: 'not-used' }),
},
'../../cliproxy/services/remote-auth-fetcher': {
fetchRemoteAuthStatus: async () => [],
},
'../../cliproxy/proxy/proxy-target-resolver': {
getProxyTarget: () => ({
host: '127.0.0.1',
port: 8317,
protocol: 'http',
isRemote: false,
}),
},
'../../cliproxy/provider-capabilities': {
getProviderDisplayName: () => 'GitHub Copilot (OAuth)',
isCLIProxyProvider: () => true,
},
'../../cliproxy/services/stats-fetcher': {
isCliproxyRunning: async () => true,
},
'../../config/unified-config-types': {},
'../../config/config-loader-facade': {
getImageAnalysisConfig: () => ({
enabled: true,
timeout: 60,
provider_models: { ghcp: 'claude-haiku-4.5' },
fallback_backend: 'ghcp',
profile_backends: {},
}),
},
'./image-analysis-backend-resolver': {
resolveImageAnalysisStatus: () => createStatus(),
},
};
const compiledModule = new Function(
'exports',
'require',
'module',
'__filename',
'__dirname',
transpiled
);
compiledModule(
module.exports,
(specifier: string) => {
const dependency = requireMap[specifier];
if (!dependency) {
throw new Error(`Unexpected dependency: ${specifier}`);
}
return dependency;
},
module,
sourcePath.pathname,
dirname(sourcePath.pathname)
);
authHandlerModule.initializeAccounts = () => {};
authHandlerModule.getAuthStatus = () => ({
provider: 'ghcp',
authenticated: true,
tokenDir: '/tmp/auth',
tokenFiles: ['github-copilot-test.json'],
accounts: [],
defaultAccount: undefined,
});
const { hydrateImageAnalysisRuntimeStatus } = module.exports as {
hydrateImageAnalysisRuntimeStatus: (
baseStatus: ImageAnalysisStatus,
deps?: Record<string, unknown>
) => Promise<ImageAnalysisStatus>;
};
const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {});
expect(status.authReadiness).toBe('ready');
expect(status.proxyReadiness).toBe('ready');
expect(status.effectiveRuntimeMode).toBe('cliproxy-image-analysis');
});
});