Files
ccs/tests/unit/utils/hooks/image-analysis-runtime-status.test.ts

229 lines
7.7 KiB
TypeScript

import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { describe, expect, it } from 'bun:test';
import { hydrateImageAnalysisRuntimeStatus } from '../../../../src/utils/hooks/image-analysis-runtime-status';
import type { ImageAnalysisStatus } from '../../../../src/utils/hooks/image-analysis-backend-resolver';
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', () => {
it('falls back to native read when provider auth is missing', async () => {
const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {
checkRemoteProxy: async () => ({ reachable: false, error: 'not-used' }),
getProxyTarget: () => ({
host: '127.0.0.1',
port: 8317,
protocol: 'http',
isRemote: false,
}),
initializeAccounts: () => {},
getAuthStatus: () => ({
provider: 'ghcp',
authenticated: false,
tokenDir: '/tmp/auth',
tokenFiles: [],
accounts: [],
defaultAccount: undefined,
}),
isCliproxyRunning: async () => true,
});
expect(status.authReadiness).toBe('missing');
expect(status.effectiveRuntimeMode).toBe('native-read');
expect(status.effectiveRuntimeReason).toContain('ccs ghcp --auth');
});
it('marks an idle local proxy as launchable when auth is ready', async () => {
const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {
checkRemoteProxy: async () => ({ reachable: false, error: 'not-used' }),
getProxyTarget: () => ({
host: '127.0.0.1',
port: 8317,
protocol: 'http',
isRemote: false,
}),
initializeAccounts: () => {},
getAuthStatus: () => ({
provider: 'ghcp',
authenticated: true,
tokenDir: '/tmp/auth',
tokenFiles: ['github-copilot-test.json'],
accounts: [],
defaultAccount: undefined,
}),
isCliproxyRunning: async () => false,
});
expect(status.authReadiness).toBe('ready');
expect(status.proxyReadiness).toBe('stopped');
expect(status.effectiveRuntimeMode).toBe('cliproxy-image-analysis');
});
it('uses getAuthStatus when initializeAccounts is omitted from an override', async () => {
const tempCcsHome = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-runtime-status-'));
const originalCcsHome = process.env.CCS_HOME;
const originalCcsDir = process.env.CCS_DIR;
delete process.env.CCS_DIR;
process.env.CCS_HOME = tempCcsHome;
try {
const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {
getProxyTarget: () => ({ host: '127.0.0.1', port: 8317, protocol: 'http', isRemote: false }),
initializeAccounts: undefined as unknown as () => void,
getAuthStatus: () => ({
provider: 'ghcp',
authenticated: true,
tokenDir: '/tmp/auth',
tokenFiles: ['github-copilot-test.json'],
accounts: [],
defaultAccount: undefined,
}),
isCliproxyRunning: async () => true,
});
expect(status.authReadiness).toBe('ready');
expect(status.effectiveRuntimeMode).toBe('cliproxy-image-analysis');
} finally {
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
else process.env.CCS_HOME = originalCcsHome;
if (originalCcsDir === undefined) delete process.env.CCS_DIR;
else process.env.CCS_DIR = originalCcsDir;
rmSync(tempCcsHome, { recursive: true, force: true });
}
});
it('treats an unreachable remote proxy as unavailable', async () => {
const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {
getProxyTarget: () => ({
host: 'remote.example',
port: 443,
protocol: 'https',
authToken: 'token',
managementKey: 'secret',
isRemote: true,
}),
fetchRemoteAuthStatus: async () => [
{
provider: 'ghcp',
displayName: 'GitHub Copilot (OAuth)',
authenticated: true,
tokenFiles: 1,
accounts: [],
defaultAccount: null,
source: 'remote',
},
],
checkRemoteProxy: async () => ({
reachable: false,
error: 'Remote CLIProxy target remote.example:443 is unreachable.',
}),
});
expect(status.authReadiness).toBe('ready');
expect(status.proxyReadiness).toBe('unavailable');
expect(status.effectiveRuntimeMode).toBe('native-read');
expect(status.effectiveRuntimeReason).toContain('remote.example:443');
});
it('keeps hook-missing as a degraded fallback state while primary runtime stays active', async () => {
const status = await hydrateImageAnalysisRuntimeStatus(
createStatus({
status: 'hook-missing',
reason: 'Profile hook is missing from the persisted settings file.',
}),
{
checkRemoteProxy: async () => ({ reachable: false, error: 'not-used' }),
getProxyTarget: () => ({
host: '127.0.0.1',
port: 8317,
protocol: 'http',
isRemote: false,
}),
initializeAccounts: () => {},
getAuthStatus: () => ({
provider: 'ghcp',
authenticated: true,
tokenDir: '/tmp/auth',
tokenFiles: ['github-copilot-test.json'],
accounts: [],
defaultAccount: undefined,
}),
isCliproxyRunning: async () => true,
}
);
expect(status.authReadiness).toBe('ready');
expect(status.proxyReadiness).toBe('ready');
expect(status.effectiveRuntimeMode).toBe('cliproxy-image-analysis');
expect(status.effectiveRuntimeReason).toContain('Profile hook is missing');
});
it('marks a reachable remote proxy as remote instead of inspecting local 8317', async () => {
const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {
getProxyTarget: () => ({
host: 'remote.example',
port: 443,
protocol: 'https',
authToken: 'token',
managementKey: 'secret',
allowSelfSigned: true,
isRemote: true,
}),
fetchRemoteAuthStatus: async () => [
{
provider: 'ghcp',
displayName: 'GitHub Copilot (OAuth)',
authenticated: true,
tokenFiles: 1,
accounts: [],
defaultAccount: null,
source: 'remote',
},
],
checkRemoteProxy: async () => ({
reachable: true,
latencyMs: 42,
}),
isCliproxyRunning: async () => false,
});
expect(status.proxyReadiness).toBe('remote');
expect(status.proxyReason).toContain('remote.example:443');
expect(status.effectiveRuntimeMode).toBe('cliproxy-image-analysis');
});
});