mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-10 22:23:35 +00:00
fix(ui): gate preset catalog reuse on query readiness
- only reuse provider catalogs after /api/cliproxy/catalog has resolved - avoid passing synthesized static fallback catalogs into preset application - cover quick-setup and cliproxy add-account flows with regression tests
This commit is contained in:
@@ -61,6 +61,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
|
|||||||
);
|
);
|
||||||
const accounts = useMemo(() => providerAuth?.accounts || [], [providerAuth?.accounts]);
|
const accounts = useMemo(() => providerAuth?.accounts || [], [providerAuth?.accounts]);
|
||||||
const catalogs = useMemo(() => buildUiCatalogs(catalogData?.catalogs), [catalogData?.catalogs]);
|
const catalogs = useMemo(() => buildUiCatalogs(catalogData?.catalogs), [catalogData?.catalogs]);
|
||||||
|
const fetchedCatalogsReady = Boolean(catalogData);
|
||||||
|
|
||||||
// Reset on close
|
// Reset on close
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -104,7 +105,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
|
|||||||
const result = await applyDefaultPreset(
|
const result = await applyDefaultPreset(
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
undefined,
|
undefined,
|
||||||
catalogs[selectedProvider]
|
fetchedCatalogsReady ? catalogs[selectedProvider] : undefined
|
||||||
);
|
);
|
||||||
if (result.success && result.presetName) {
|
if (result.success && result.presetName) {
|
||||||
toast.success(`Applied "${result.presetName}" preset`);
|
toast.success(`Applied "${result.presetName}" preset`);
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export function CliproxyPage() {
|
|||||||
const isRemoteMode = authData?.source === 'remote';
|
const isRemoteMode = authData?.source === 'remote';
|
||||||
const variants = useMemo(() => variantsData?.variants || [], [variantsData?.variants]);
|
const variants = useMemo(() => variantsData?.variants || [], [variantsData?.variants]);
|
||||||
const catalogs = useMemo(() => buildUiCatalogs(catalogData?.catalogs), [catalogData?.catalogs]);
|
const catalogs = useMemo(() => buildUiCatalogs(catalogData?.catalogs), [catalogData?.catalogs]);
|
||||||
|
const fetchedCatalogsReady = Boolean(catalogData);
|
||||||
|
|
||||||
// Wrapper to persist provider selection to localStorage
|
// Wrapper to persist provider selection to localStorage
|
||||||
const setSelectedProvider = (provider: string | null) => {
|
const setSelectedProvider = (provider: string | null) => {
|
||||||
@@ -561,7 +562,11 @@ export function CliproxyPage() {
|
|||||||
onClose={() => setAddAccountProvider(null)}
|
onClose={() => setAddAccountProvider(null)}
|
||||||
provider={addAccountProvider?.provider || ''}
|
provider={addAccountProvider?.provider || ''}
|
||||||
displayName={addAccountProvider?.displayName || ''}
|
displayName={addAccountProvider?.displayName || ''}
|
||||||
catalog={addAccountProvider?.provider ? catalogs[addAccountProvider.provider] : undefined}
|
catalog={
|
||||||
|
fetchedCatalogsReady && addAccountProvider?.provider
|
||||||
|
? catalogs[addAccountProvider.provider]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
isFirstAccount={addAccountProvider?.isFirstAccount || false}
|
isFirstAccount={addAccountProvider?.isFirstAccount || false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils';
|
||||||
|
|
||||||
|
const hookState = vi.hoisted(() => ({
|
||||||
|
catalogData: undefined as
|
||||||
|
| {
|
||||||
|
catalogs: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
provider: string;
|
||||||
|
displayName: string;
|
||||||
|
defaultModel: string;
|
||||||
|
models: Array<{ id: string; name: string }>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
| undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const applyDefaultPresetMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-cliproxy', () => ({
|
||||||
|
useCliproxyAuth: () => ({
|
||||||
|
data: {
|
||||||
|
authStatus: [
|
||||||
|
{
|
||||||
|
provider: 'gemini',
|
||||||
|
displayName: 'Gemini',
|
||||||
|
authenticated: false,
|
||||||
|
accounts: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
refetch: vi.fn(),
|
||||||
|
}),
|
||||||
|
useCliproxyCatalog: () => ({
|
||||||
|
data: hookState.catalogData,
|
||||||
|
}),
|
||||||
|
useCreateVariant: () => ({
|
||||||
|
isPending: false,
|
||||||
|
mutateAsync: vi.fn(),
|
||||||
|
}),
|
||||||
|
useStartAuth: () => ({
|
||||||
|
isPending: false,
|
||||||
|
mutate: (_args: unknown, options?: { onSuccess?: (data: Record<string, unknown>) => void }) => {
|
||||||
|
options?.onSuccess?.({});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useCancelAuth: () => ({
|
||||||
|
mutate: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/lib/preset-utils', () => ({
|
||||||
|
applyDefaultPreset: applyDefaultPresetMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/setup/wizard/steps/provider-step', () => ({
|
||||||
|
ProviderStep: ({ onSelect }: { onSelect: (providerId: string) => void }) => (
|
||||||
|
<button onClick={() => onSelect('gemini')}>choose-gemini</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/setup/wizard/steps/auth-step', () => ({
|
||||||
|
AuthStep: ({ onStartAuth }: { onStartAuth: () => void }) => (
|
||||||
|
<button onClick={onStartAuth}>start-auth</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/setup/wizard/steps/account-step', () => ({
|
||||||
|
AccountStep: () => <div>account-step</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/setup/wizard/steps/variant-step', () => ({
|
||||||
|
VariantStep: () => <div>variant-step</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/setup/wizard/steps/success-step', () => ({
|
||||||
|
SuccessStep: () => <div>success-step</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { QuickSetupWizard } from '@/components/setup/wizard';
|
||||||
|
|
||||||
|
describe('QuickSetupWizard preset catalog reuse', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hookState.catalogData = undefined;
|
||||||
|
applyDefaultPresetMock.mockReset();
|
||||||
|
applyDefaultPresetMock.mockResolvedValue({ success: true, presetName: 'Gemini Pro' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not pass a synthesized static catalog before the catalog query resolves', async () => {
|
||||||
|
render(<QuickSetupWizard open onClose={vi.fn()} />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'choose-gemini' }));
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'start-auth' }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(applyDefaultPresetMock).toHaveBeenCalledWith('gemini', undefined, undefined)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the fetched provider catalog once the query has resolved', async () => {
|
||||||
|
hookState.catalogData = {
|
||||||
|
catalogs: {
|
||||||
|
gemini: {
|
||||||
|
provider: 'gemini',
|
||||||
|
displayName: 'Gemini',
|
||||||
|
defaultModel: 'gemini-3.9-pro-preview',
|
||||||
|
models: [{ id: 'gemini-3.9-pro-preview', name: 'Gemini 3.9 Pro Preview' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<QuickSetupWizard open onClose={vi.fn()} />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'choose-gemini' }));
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'start-auth' }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(applyDefaultPresetMock).toHaveBeenCalledWith(
|
||||||
|
'gemini',
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({
|
||||||
|
defaultModel: 'gemini-3.9-pro-preview',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { render, screen, userEvent } from '@tests/setup/test-utils';
|
||||||
|
|
||||||
|
const hookState = vi.hoisted(() => ({
|
||||||
|
catalogData: undefined as
|
||||||
|
| {
|
||||||
|
catalogs: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
provider: string;
|
||||||
|
displayName: string;
|
||||||
|
defaultModel: string;
|
||||||
|
models: Array<{ id: string; name: string }>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
| undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-cliproxy', () => ({
|
||||||
|
useCliproxy: () => ({
|
||||||
|
data: { variants: [] },
|
||||||
|
isFetching: false,
|
||||||
|
}),
|
||||||
|
useCliproxyAuth: () => ({
|
||||||
|
data: {
|
||||||
|
authStatus: [
|
||||||
|
{
|
||||||
|
provider: 'gemini',
|
||||||
|
displayName: 'Gemini',
|
||||||
|
authenticated: true,
|
||||||
|
accounts: [{ id: 'acct-1', provider: 'gemini' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
source: 'local',
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
useCliproxyCatalog: () => ({
|
||||||
|
data: hookState.catalogData,
|
||||||
|
}),
|
||||||
|
useCliproxyUpdateCheck: () => ({
|
||||||
|
data: undefined,
|
||||||
|
}),
|
||||||
|
useSetDefaultAccount: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
useRemoveAccount: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
usePauseAccount: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
useResumeAccount: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
useSoloAccount: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
useBulkPauseAccounts: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
useBulkResumeAccounts: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
useDeleteVariant: () => ({ mutate: vi.fn(), isPending: false }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/quick-setup-wizard', () => ({
|
||||||
|
QuickSetupWizard: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/monitoring/proxy-status-widget', () => ({
|
||||||
|
ProxyStatusWidget: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/account/account-safety-warning-card', () => ({
|
||||||
|
AccountSafetyWarningCard: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/cliproxy/provider-logo', () => ({
|
||||||
|
ProviderLogo: () => <div>provider-logo</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/cliproxy/provider-editor', () => ({
|
||||||
|
ProviderEditor: ({ onAddAccount }: { onAddAccount: () => void }) => (
|
||||||
|
<button onClick={onAddAccount}>open-add-account</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/account/add-account-dialog', () => ({
|
||||||
|
AddAccountDialog: ({ open, catalog }: { open: boolean; catalog?: { defaultModel?: string } }) =>
|
||||||
|
open ? (
|
||||||
|
<div data-testid="add-account-dialog" data-catalog={catalog?.defaultModel ?? 'missing'} />
|
||||||
|
) : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { CliproxyPage } from '@/pages/cliproxy';
|
||||||
|
|
||||||
|
describe('CliproxyPage add-account catalog gating', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hookState.catalogData = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not pass a static fallback catalog before the catalog query resolves', async () => {
|
||||||
|
render(<CliproxyPage />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'open-add-account' }));
|
||||||
|
|
||||||
|
expect(screen.getByTestId('add-account-dialog')).toHaveAttribute('data-catalog', 'missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the fetched provider catalog after the catalog query resolves', async () => {
|
||||||
|
hookState.catalogData = {
|
||||||
|
catalogs: {
|
||||||
|
gemini: {
|
||||||
|
provider: 'gemini',
|
||||||
|
displayName: 'Gemini',
|
||||||
|
defaultModel: 'gemini-3.9-pro-preview',
|
||||||
|
models: [{ id: 'gemini-3.9-pro-preview', name: 'Gemini 3.9 Pro Preview' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<CliproxyPage />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'open-add-account' }));
|
||||||
|
|
||||||
|
expect(screen.getByTestId('add-account-dialog')).toHaveAttribute(
|
||||||
|
'data-catalog',
|
||||||
|
'gemini-3.9-pro-preview'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user