mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-20 05:11:48 +00:00
feat(ui): clarify image-analysis target status
This commit is contained in:
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user