feat(ui): clarify image-analysis target status

This commit is contained in:
Tam Nhu Tran committed 2026-04-01 15:32:37 -04:00
1 parent 3b61673ad2
commit 0246e327fe
10 files changed
+495 -84

No files matched your search

@@ -80,17 +80,25 @@ describe('ImageAnalysisStatusSection', () => {
it('renders saved diagnostics for an active backend', () => {
render(<ImageAnalysisStatusSection status={createStatus()} />);
expect(screen.getByText('Image-analysis backend')).toBeInTheDocument();
expect(screen.getByText('Image Analysis')).toBeInTheDocument();
expect(
screen.getByText(
/Saved runtime status for this profile\. Config stays in the JSON editor above; auth and proxy readiness are derived at runtime\./i
)
).toBeInTheDocument();
expect(screen.getByText('Ready')).toBeInTheDocument();
expect(screen.getByText('Saved')).toBeInTheDocument();
expect(screen.getByText('CLIProxy Active')).toBeInTheDocument();
expect(
screen.getByText(/Configured via Google Gemini\. Image and PDF reads use CLIProxy/i)
screen.getByText(
/Images and PDFs for this profile resolve through Google Gemini via CLIProxy\./i
)
).toBeInTheDocument();
expect(screen.getByText('Configured backend')).toBeInTheDocument();
expect(screen.getByText('Effective runtime')).toBeInTheDocument();
expect(screen.getByText('Hook persistence')).toBeInTheDocument();
expect(screen.getByText('Google Gemini')).toBeInTheDocument();
expect(screen.getByText('CLIProxy image analysis')).toBeInTheDocument();
expect(screen.getByText('Hook saved to profile')).toBeInTheDocument();
expect(screen.getByTitle(/\/api\/provider\/gemini/)).toBeInTheDocument();
expect(screen.getAllByText('Google Gemini ready')).toHaveLength(1);
expect(screen.getByText('Local CLIProxy ready')).toBeInTheDocument();
@@ -112,13 +120,14 @@ describe('ImageAnalysisStatusSection', () => {
/>
);
expect(screen.getByText('Ready via mapping')).toBeInTheDocument();
expect(screen.getByText('CLIProxy Active')).toBeInTheDocument();
expect(
screen.getByText(
/Configured via saved GitHub Copilot \(OAuth\) mapping\. Auth and runtime are ready/i
/Images and PDFs for this profile resolve through GitHub Copilot \(OAuth\) via a saved Image Analysis mapping\./i
)
).toBeInTheDocument();
expect(screen.getByText('GitHub Copilot (OAuth)')).toBeInTheDocument();
expect(screen.getByText('Saved Image Analysis mapping')).toBeInTheDocument();
expect(screen.getByText('claude-haiku-4.5')).toBeInTheDocument();
});
@@ -136,8 +145,11 @@ describe('ImageAnalysisStatusSection', () => {
expect(screen.getByText('Setup needed')).toBeInTheDocument();
expect(
screen.getByText(/Configured for Google Gemini, but Profile hook is missing/i)
screen.getByText(
/Images and PDFs are configured for Google Gemini, but Profile hook is missing/i
)
).toBeInTheDocument();
expect(screen.getByText('Native file access')).toBeInTheDocument();
expect(screen.getByTitle(/native file access/i)).toBeInTheDocument();
});
@@ -163,11 +175,12 @@ describe('ImageAnalysisStatusSection', () => {
expect(screen.getByText('Needs auth')).toBeInTheDocument();
expect(
screen.getByText(
/Configured via GitHub Copilot \(OAuth\), but GitHub Copilot \(OAuth\) auth is missing/i
)
screen.getByText(/This profile currently falls back to native file access\./i)
).toBeInTheDocument();
expect(screen.getAllByText(/Run "ccs ghcp --auth" to enable image analysis/i)).toHaveLength(3);
expect(screen.getByText('Native file access')).toBeInTheDocument();
expect(
screen.getAllByText(/Run "ccs ghcp --auth" to enable image analysis/i).length
).toBeGreaterThanOrEqual(3);
});
it('treats an idle local proxy as launchable instead of unavailable', () => {
@@ -183,12 +196,81 @@ describe('ImageAnalysisStatusSection', () => {
expect(screen.getByText('Starts on launch')).toBeInTheDocument();
expect(
screen.getByText(/Auth is ready and CCS will start the local CLIProxy runtime on launch/i)
screen.getByText(
/Images and PDFs for this profile resolve through Google Gemini\. Auth is ready and CCS will start the local CLIProxy runtime when needed\./i
)
).toBeInTheDocument();
expect(screen.getByText('Local CLIProxy idle; starts on launch')).toBeInTheDocument();
expect(screen.getByText('Auth ready • Local CLIProxy starts on demand')).toBeInTheDocument();
expect(screen.getByTitle(/start local CLIProxy/i)).toBeInTheDocument();
});
it('shows saved Claude-side diagnostics when the current target bypasses the hook', () => {
render(<ImageAnalysisStatusSection status={createStatus()} target="droid" />);
expect(screen.getByText('Target bypassed')).toBeInTheDocument();
expect(
screen.getByText(
/Images and PDFs for this profile are configured to resolve through Google Gemini when the profile runs on Claude Code\./i
)
).toBeInTheDocument();
expect(
screen.getByText(
/Current default target: Factory Droid\. The diagnostics below describe the saved Claude-side Image Analysis hook/i
)
).toBeInTheDocument();
expect(screen.getByText('Not active on Factory Droid')).toBeInTheDocument();
expect(
screen.getByText(
/Factory Droid bypasses the Claude Read hook\. Switch the target back to Claude Code to use the saved backend shown here\./i
)
).toBeInTheDocument();
expect(screen.getByTitle(/Factory Droid launch -> no Claude Read hook/i)).toBeInTheDocument();
});
it('keeps saved Claude-side auth failures visible when another target bypasses the hook', () => {
render(
<ImageAnalysisStatusSection
status={createStatus({
backendId: 'ghcp',
backendDisplayName: 'GitHub Copilot (OAuth)',
authReadiness: 'missing',
authProvider: 'ghcp',
authDisplayName: 'GitHub Copilot (OAuth)',
authReason:
'GitHub Copilot (OAuth) auth is missing. Run "ccs ghcp --auth" to enable image analysis.',
effectiveRuntimeMode: 'native-read',
effectiveRuntimeReason:
'GitHub Copilot (OAuth) auth is missing. Run "ccs ghcp --auth" to enable image analysis.',
})}
target="codex"
/>
);
expect(screen.getByText('Needs auth')).toBeInTheDocument();
expect(
screen.getByText(
/Current default target: Codex CLI\. This launch path bypasses the Claude Read hook, and the saved Claude-side setup for GitHub Copilot \(OAuth\) still falls back to native file access\./i
)
).toBeInTheDocument();
expect(
screen.getAllByText(/Run "ccs ghcp --auth" to enable image analysis/i).length
).toBeGreaterThanOrEqual(1);
});
it('falls back to backend id when a display name is unavailable', () => {
render(
<ImageAnalysisStatusSection
status={createStatus({
backendDisplayName: null,
backendId: 'ghcp',
})}
/>
);
expect(screen.getByText('ghcp')).toBeInTheDocument();
});
it('switches the panel to a live preview when the current editor JSON changes', async () => {
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
@@ -305,4 +387,123 @@ describe('ImageAnalysisStatusSection', () => {
});
expect(screen.getByText('Google Gemini')).toBeInTheDocument();
});
it('marks the preview as refreshing when a newer editor preview is still loading', async () => {
let secondPreviewResolver: ((value: Response) => void) | null = null;
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.includes('/api/settings/glm/raw')) {
return Promise.resolve(
createJsonResponse({
profile: 'glm',
settings: {
env: {
ANTHROPIC_BASE_URL: 'https://api.z.ai/v1',
ANTHROPIC_AUTH_TOKEN: 'saved-token',
},
},
mtime: 1,
path: '/tmp/glm.settings.json',
imageAnalysisStatus: createStatus(),
})
);
}
if (url.includes('/api/settings/glm/image-analysis-status')) {
expect(init?.method).toBe('POST');
const body = JSON.parse(String(init?.body ?? '{}')) as {
settings?: { env?: Record<string, string> };
};
const baseUrl = body.settings?.env?.ANTHROPIC_BASE_URL ?? '';
if (baseUrl.includes('/ghcp')) {
return Promise.resolve(
createJsonResponse({
imageAnalysisStatus: createStatus({
backendId: 'ghcp',
backendDisplayName: 'GitHub Copilot (OAuth)',
model: 'claude-haiku-4.5',
runtimePath: '/api/provider/ghcp',
authReadiness: 'ready',
authProvider: 'ghcp',
authDisplayName: 'GitHub Copilot (OAuth)',
}),
})
);
}
if (baseUrl.includes('/codex')) {
return new Promise<Response>((resolve) => {
secondPreviewResolver = resolve;
});
}
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
});
vi.stubGlobal('fetch', fetchMock);
render(<ProfileEditor profileName="glm" profileTarget="claude" />);
expect(await screen.findByText('Google Gemini')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('raw config editor'), {
target: {
value: JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'https://proxy.example/api/provider/ghcp',
ANTHROPIC_AUTH_TOKEN: 'preview-token',
},
},
null,
2
),
},
});
expect(await screen.findByText('GitHub Copilot (OAuth)')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('raw config editor'), {
target: {
value: JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'https://proxy.example/api/provider/codex',
ANTHROPIC_AUTH_TOKEN: 'preview-token-2',
},
},
null,
2
),
},
});
await waitFor(() => {
expect(screen.getByText('Refreshing')).toBeInTheDocument();
});
expect(
screen.getByText(/Refreshing the live preview from the current editor state\./i)
).toBeInTheDocument();
secondPreviewResolver?.(
createJsonResponse({
imageAnalysisStatus: createStatus({
backendId: 'codex',
backendDisplayName: 'Codex',
model: 'gpt-5.4',
runtimePath: '/api/provider/codex',
authReadiness: 'ready',
authProvider: 'codex',
authDisplayName: 'Codex',
}),
})
);
await waitFor(() => {
expect(screen.getByText('Codex')).toBeInTheDocument();
});
});
});