import { describe, expect, it } from 'bun:test'; import { DEFAULT_IMAGE_ANALYSIS_CONFIG, type ImageAnalysisConfig, } from '../../../../src/config/unified-config-types'; import { canonicalizeImageAnalysisConfig, resolveImageAnalysisStatus, } from '../../../../src/utils/hooks/image-analysis-backend-resolver'; describe('image-analysis-backend-resolver', () => { it('canonicalizes provider aliases in config', () => { const config = canonicalizeImageAnalysisConfig({ enabled: true, timeout: 60, provider_models: { copilot: 'claude-haiku-4.5', gemini: 'gemini-2.5-flash', }, fallback_backend: 'Gemini', profile_backends: { orq: 'copilot', }, }); expect(config.provider_models.ghcp).toBe('claude-haiku-4.5'); expect(config.provider_models.copilot).toBeUndefined(); expect(config.fallback_backend).toBe('gemini'); expect(config.profile_backends?.orq).toBe('ghcp'); }); it('resolves copilot to the ghcp backend without a duplicate provider key', () => { const status = resolveImageAnalysisStatus( { profileName: 'copilot', profileType: 'copilot', }, DEFAULT_IMAGE_ANALYSIS_CONFIG ); expect(status.supported).toBe(true); expect(status.backendId).toBe('ghcp'); expect(status.model).toBe('claude-haiku-4.5'); expect(status.resolutionSource).toBe('copilot-alias'); }); it('does not route cursor image analysis through the fallback backend by default', () => { const status = resolveImageAnalysisStatus( { profileName: 'cursor', profileType: 'cursor', }, DEFAULT_IMAGE_ANALYSIS_CONFIG ); expect(status.supported).toBe(false); expect(status.backendId).toBeNull(); expect(status.status).toBe('skipped'); expect(status.resolutionSource).toBe('unresolved'); expect(status.reason).toContain('profile_backends.cursor'); }); it('allows cursor image analysis only when explicitly mapped to a backend', () => { const config: ImageAnalysisConfig = { ...DEFAULT_IMAGE_ANALYSIS_CONFIG, profile_backends: { cursor: 'ghcp', }, }; const status = resolveImageAnalysisStatus( { profileName: 'cursor', profileType: 'cursor', }, config ); expect(status.supported).toBe(true); expect(status.backendId).toBe('ghcp'); expect(status.status).toBe('mapped'); expect(status.resolutionSource).toBe('profile-backend'); }); it('treats cliproxy cursor as a provider-backed profile rather than a legacy bridge alias', () => { const status = resolveImageAnalysisStatus( { profileName: 'cursor', profileType: 'cliproxy', cliproxyProvider: 'cursor', }, DEFAULT_IMAGE_ANALYSIS_CONFIG ); expect(status.backendId).toBe('cursor'); expect(status.resolutionSource).toBe('cliproxy-provider'); expect(status.reason).toContain('no image-analysis model configured'); }); it('uses the fallback backend for an unmapped third-party settings profile', () => { const status = resolveImageAnalysisStatus( { profileName: 'glm', profileType: 'settings', settings: { env: { ANTHROPIC_BASE_URL: 'https://api.z.ai/v1', ANTHROPIC_AUTH_TOKEN: 'glm-test-key', }, }, }, DEFAULT_IMAGE_ANALYSIS_CONFIG ); expect(status.supported).toBe(true); expect(status.backendId).toBe('gemini'); expect(status.resolutionSource).toBe('fallback-backend'); expect(status.model).toBe('gemini-3-flash-preview'); }); it('keeps direct Anthropic settings profiles on native read unless explicitly mapped', () => { const status = resolveImageAnalysisStatus( { profileName: 'claude-direct', profileType: 'settings', settings: { env: { ANTHROPIC_API_KEY: 'anthropic-test-key', }, }, }, DEFAULT_IMAGE_ANALYSIS_CONFIG ); expect(status.supported).toBe(false); expect(status.backendId).toBeNull(); expect(status.status).toBe('skipped'); expect(status.shouldPersistHook).toBe(false); expect(status.reason).toContain('native file access'); }); it('uses explicit profile_backends overrides for custom aliases', () => { const config: ImageAnalysisConfig = { ...DEFAULT_IMAGE_ANALYSIS_CONFIG, profile_backends: { orq: 'copilot', }, }; const status = resolveImageAnalysisStatus( { profileName: 'orq', profileType: 'settings', }, config ); expect(status.supported).toBe(true); expect(status.status).toBe('mapped'); expect(status.backendId).toBe('ghcp'); expect(status.resolutionSource).toBe('profile-backend'); }); it('lets explicit profile_backends overrides win over cliproxy provider inference', () => { const config: ImageAnalysisConfig = { ...DEFAULT_IMAGE_ANALYSIS_CONFIG, profile_backends: { glmv: 'ghcp', }, }; const status = resolveImageAnalysisStatus( { profileName: 'glmv', profileType: 'cliproxy', cliproxyProvider: 'gemini', }, config ); expect(status.supported).toBe(true); expect(status.status).toBe('mapped'); expect(status.backendId).toBe('ghcp'); expect(status.resolutionSource).toBe('profile-backend'); }); it('reports hook-missing when the profile should persist a hook but it is absent', () => { const status = resolveImageAnalysisStatus( { profileName: 'glm', profileType: 'settings', settings: { env: { ANTHROPIC_BASE_URL: 'https://api.z.ai/v1', ANTHROPIC_AUTH_TOKEN: 'glm-test-key', }, }, hookInstalled: false, sharedHookInstalled: true, }, DEFAULT_IMAGE_ANALYSIS_CONFIG ); expect(status.status).toBe('hook-missing'); expect(status.reason).toContain('Profile hook is missing'); }); it('prefers native image reading when the profile settings opt into it', () => { const status = resolveImageAnalysisStatus( { profileName: 'glmv', profileType: 'settings', settings: { env: { ANTHROPIC_BASE_URL: 'https://api.z.ai/v1', ANTHROPIC_MODEL: 'glm-4.5v', ANTHROPIC_AUTH_TOKEN: 'glm-test-key', }, ccs_image: { native_read: true, }, }, }, DEFAULT_IMAGE_ANALYSIS_CONFIG ); expect(status.backendId).toBeNull(); expect(status.resolutionSource).toBe('native-compatible'); expect(status.nativeReadPreference).toBe(true); expect(status.profileModel).toBe('glm-4.5v'); expect(status.nativeImageCapable).toBe(true); expect(status.shouldPersistHook).toBe(false); expect(status.effectiveRuntimeMode).toBe('native-read'); }); });