mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 00:14:37 +00:00
fix(image-analysis): surface runtime readiness
This commit is contained in:
1 parent
d394772f7c
commit
1a01c6fc68
12 files changed
+788
-123
No files matched your search
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { generateCopilotEnv } from '../../../src/copilot/copilot-executor';
|
||||
import {
|
||||
generateCopilotEnv,
|
||||
resolveCopilotImageAnalysisEnv,
|
||||
} from '../../../src/copilot/copilot-executor';
|
||||
import type { CopilotConfig } from '../../../src/config/unified-config-types';
|
||||
|
||||
const baseConfig: CopilotConfig = {
|
||||
@@ -50,4 +53,94 @@ describe('generateCopilotEnv', () => {
|
||||
const env = generateCopilotEnv(baseConfig);
|
||||
expect(env.CLAUDE_CONFIG_DIR).toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to native read when copilot image analysis auth is missing', async () => {
|
||||
const result = await resolveCopilotImageAnalysisEnv(false, {
|
||||
getImageAnalysisHookEnv: () => ({
|
||||
CCS_CURRENT_PROVIDER: 'ghcp',
|
||||
CCS_IMAGE_ANALYSIS_SKIP: '0',
|
||||
}),
|
||||
resolveImageAnalysisRuntimeStatus: async () => ({
|
||||
enabled: true,
|
||||
supported: true,
|
||||
status: 'active',
|
||||
backendId: 'ghcp',
|
||||
backendDisplayName: 'GitHub Copilot (OAuth)',
|
||||
model: 'claude-haiku-4.5',
|
||||
resolutionSource: 'copilot-alias',
|
||||
reason: null,
|
||||
shouldPersistHook: true,
|
||||
persistencePath: 'copilot.settings.json',
|
||||
runtimePath: '/api/provider/ghcp',
|
||||
usesCurrentTarget: true,
|
||||
usesCurrentAuthToken: true,
|
||||
hookInstalled: true,
|
||||
sharedHookInstalled: true,
|
||||
authReadiness: 'missing',
|
||||
authProvider: 'ghcp',
|
||||
authDisplayName: 'GitHub Copilot (OAuth)',
|
||||
authReason:
|
||||
'GitHub Copilot (OAuth) auth is missing. Run "ccs ghcp --auth" to enable image analysis.',
|
||||
proxyReadiness: 'stopped',
|
||||
proxyReason:
|
||||
'Local CLIProxy service is idle. CCS will start it automatically when image analysis is needed.',
|
||||
effectiveRuntimeMode: 'native-read',
|
||||
effectiveRuntimeReason:
|
||||
'GitHub Copilot (OAuth) auth is missing. Run "ccs ghcp --auth" to enable image analysis.',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.env.CCS_CURRENT_PROVIDER).toBe('');
|
||||
expect(result.env.CCS_IMAGE_ANALYSIS_SKIP).toBe('1');
|
||||
expect(result.warning).toContain('ccs ghcp --auth');
|
||||
});
|
||||
|
||||
it('starts local CLIProxy on demand when copilot image analysis is launchable', async () => {
|
||||
let ensureCalls = 0;
|
||||
const result = await resolveCopilotImageAnalysisEnv(false, {
|
||||
getImageAnalysisHookEnv: () => ({
|
||||
CCS_CURRENT_PROVIDER: 'ghcp',
|
||||
CCS_IMAGE_ANALYSIS_SKIP: '0',
|
||||
}),
|
||||
resolveImageAnalysisRuntimeStatus: async () => ({
|
||||
enabled: true,
|
||||
supported: true,
|
||||
status: 'active',
|
||||
backendId: 'ghcp',
|
||||
backendDisplayName: 'GitHub Copilot (OAuth)',
|
||||
model: 'claude-haiku-4.5',
|
||||
resolutionSource: 'copilot-alias',
|
||||
reason: null,
|
||||
shouldPersistHook: true,
|
||||
persistencePath: 'copilot.settings.json',
|
||||
runtimePath: '/api/provider/ghcp',
|
||||
usesCurrentTarget: true,
|
||||
usesCurrentAuthToken: true,
|
||||
hookInstalled: true,
|
||||
sharedHookInstalled: true,
|
||||
authReadiness: 'ready',
|
||||
authProvider: 'ghcp',
|
||||
authDisplayName: 'GitHub Copilot (OAuth)',
|
||||
authReason: null,
|
||||
proxyReadiness: 'stopped',
|
||||
proxyReason:
|
||||
'Local CLIProxy service is idle. CCS will start it automatically when image analysis is needed.',
|
||||
effectiveRuntimeMode: 'cliproxy-image-analysis',
|
||||
effectiveRuntimeReason: null,
|
||||
}),
|
||||
ensureCliproxyService: async () => {
|
||||
ensureCalls += 1;
|
||||
return {
|
||||
started: true,
|
||||
alreadyRunning: false,
|
||||
port: 8317,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(ensureCalls).toBe(1);
|
||||
expect(result.env.CCS_CURRENT_PROVIDER).toBe('ghcp');
|
||||
expect(result.env.CCS_IMAGE_ANALYSIS_SKIP).toBe('0');
|
||||
expect(result.warning).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
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,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('image-analysis-runtime-status', () => {
|
||||
it('falls back to native read when provider auth is missing', async () => {
|
||||
const status = await hydrateImageAnalysisRuntimeStatus(createStatus(), {
|
||||
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(), {
|
||||
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('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',
|
||||
},
|
||||
],
|
||||
isCliproxyRunning: async () => false,
|
||||
});
|
||||
|
||||
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 on native read even when auth and proxy are ready', async () => {
|
||||
const status = await hydrateImageAnalysisRuntimeStatus(
|
||||
createStatus({
|
||||
status: 'hook-missing',
|
||||
reason: 'Profile hook is missing from the persisted settings file.',
|
||||
}),
|
||||
{
|
||||
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('native-read');
|
||||
expect(status.effectiveRuntimeReason).toContain('Profile hook is missing');
|
||||
});
|
||||
});
|
||||
@@ -103,6 +103,8 @@ describe('settings-routes image-analysis status', () => {
|
||||
resolutionSource: string;
|
||||
model: string | null;
|
||||
persistencePath: string | null;
|
||||
authReadiness: string;
|
||||
effectiveRuntimeMode: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,6 +113,8 @@ describe('settings-routes image-analysis status', () => {
|
||||
expect(body.imageAnalysisStatus.resolutionSource).toBe('fallback-backend');
|
||||
expect(body.imageAnalysisStatus.model).toBe('gemini-2.5-flash');
|
||||
expect(body.imageAnalysisStatus.persistencePath).toContain('glm.settings.json');
|
||||
expect(body.imageAnalysisStatus.authReadiness).toBe('missing');
|
||||
expect(body.imageAnalysisStatus.effectiveRuntimeMode).toBe('native-read');
|
||||
});
|
||||
|
||||
it('keeps direct Anthropic settings profiles on native read diagnostics', async () => {
|
||||
@@ -130,6 +134,8 @@ describe('settings-routes image-analysis status', () => {
|
||||
shouldPersistHook: boolean;
|
||||
runtimePath: string | null;
|
||||
reason: string | null;
|
||||
authReadiness: string;
|
||||
proxyReadiness: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -138,6 +144,8 @@ describe('settings-routes image-analysis status', () => {
|
||||
expect(body.imageAnalysisStatus.shouldPersistHook).toBe(false);
|
||||
expect(body.imageAnalysisStatus.runtimePath).toBeNull();
|
||||
expect(body.imageAnalysisStatus.reason).toContain('native file access');
|
||||
expect(body.imageAnalysisStatus.authReadiness).toBe('not-needed');
|
||||
expect(body.imageAnalysisStatus.proxyReadiness).toBe('not-needed');
|
||||
});
|
||||
|
||||
it('returns explicit mapped status for custom aliases', async () => {
|
||||
@@ -169,6 +177,8 @@ describe('settings-routes image-analysis status', () => {
|
||||
backendId: string | null;
|
||||
resolutionSource: string;
|
||||
model: string | null;
|
||||
authReadiness: string;
|
||||
effectiveRuntimeMode: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -176,6 +186,8 @@ describe('settings-routes image-analysis status', () => {
|
||||
expect(body.imageAnalysisStatus.backendId).toBe('ghcp');
|
||||
expect(body.imageAnalysisStatus.resolutionSource).toBe('profile-backend');
|
||||
expect(body.imageAnalysisStatus.model).toBe('claude-haiku-4.5');
|
||||
expect(body.imageAnalysisStatus.authReadiness).toBe('missing');
|
||||
expect(body.imageAnalysisStatus.effectiveRuntimeMode).toBe('native-read');
|
||||
});
|
||||
|
||||
it('uses the configured custom settings path for status and persistence diagnostics', async () => {
|
||||
@@ -235,10 +247,12 @@ describe('settings-routes image-analysis status', () => {
|
||||
imageAnalysisStatus: {
|
||||
backendId: string | null;
|
||||
resolutionSource: string;
|
||||
authReadiness: string;
|
||||
};
|
||||
};
|
||||
|
||||
expect(body.imageAnalysisStatus.backendId).toBe('ghcp');
|
||||
expect(body.imageAnalysisStatus.resolutionSource).toBe('cliproxy-bridge');
|
||||
expect(body.imageAnalysisStatus.authReadiness).toBe('missing');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user