) => void
+ onRemove?: (credentialId: string) => void
+ }) => (
+
+
+
+
+
+ ),
+}))
+
+vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
+ SwitchCredentialInLoadBalancing: ({ onUpdate }: { onUpdate: () => void }) => (
+
+ ),
+}))
+
+vi.mock('../model-icon', () => ({
+ default: () => model-icon
,
+}))
+
+vi.mock('../model-name', () => ({
+ default: () => model-name
,
+}))
+
+describe('ModelLoadBalancingModal', () => {
+ const mockProvider = {
+ provider: 'test-provider',
+ provider_credential_schema: {
+ credential_form_schemas: [{ type: 'secret-input', variable: 'api_key' }],
+ },
+ model_credential_schema: {
+ credential_form_schemas: [{ type: 'secret-input', variable: 'api_key' }],
+ },
+ } as unknown as ModelProvider
+
+ const mockModel = {
+ model: 'gpt-4',
+ model_type: 'llm',
+ fetch_from: 'predefined-model',
+ } as unknown as ModelItem
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockDeleteModel = null
+ mockCredentialData = {
+ load_balancing: {
+ enabled: true,
+ configs: [
+ { id: 'cfg-1', credential_id: 'cred-1', enabled: true, name: 'Default', credentials: { api_key: 'same-key' } },
+ { id: 'cfg-2', credential_id: 'cred-2', enabled: true, name: 'Backup', credentials: { api_key: 'backup-key' } },
+ ],
+ },
+ current_credential_id: 'cred-1',
+ available_credentials: [
+ { credential_id: 'cred-1', credential_name: 'Default' },
+ { credential_id: 'cred-2', credential_name: 'Backup' },
+ ],
+ current_credential_name: 'Default',
+ }
+ mockMutateAsync.mockResolvedValue({ result: 'success' })
+ mockRefetch.mockResolvedValue({ data: mockCredentialData })
+ })
+
+ it('should show loading area while draft config is not ready', () => {
+ mockCredentialData = undefined
+
+ render(
+ ,
+ )
+
+ expect(screen.getByRole('status')).toBeInTheDocument()
+ })
+
+ it('should render predefined model content', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText(/modelProvider\.auth\.configLoadBalancing/)).toBeInTheDocument()
+ expect(screen.getByText(/modelProvider\.auth\.providerManaged$/)).toBeInTheDocument()
+ expect(screen.getByText(/operation\.save/)).toBeInTheDocument()
+ })
+
+ it('should render custom model actions and close when update has no credentials', async () => {
+ const onClose = vi.fn()
+ mockRefetch.mockResolvedValue({ data: { available_credentials: [] } })
+ render(
+ ,
+ )
+
+ expect(screen.getByText(/modelProvider\.auth\.removeModel/)).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'switch credential' })).toBeInTheDocument()
+ fireEvent.click(screen.getByRole('button', { name: 'config add credential' }))
+ await waitFor(() => {
+ expect(onClose).toHaveBeenCalled()
+ })
+ })
+
+ it('should save load balancing config and close modal', async () => {
+ const onSave = vi.fn()
+ const onClose = vi.fn()
+
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByRole('button', { name: 'config add credential' }))
+ fireEvent.click(screen.getByRole('button', { name: 'config rename credential' }))
+ fireEvent.click(screen.getByText(/operation\.save/))
+
+ await waitFor(() => {
+ expect(mockRefetch).toHaveBeenCalled()
+ expect(mockMutateAsync).toHaveBeenCalled()
+ const payload = mockMutateAsync.mock.calls[0][0] as { load_balancing: { configs: Array<{ credentials: { api_key: string } }> } }
+ expect(payload.load_balancing.configs[0].credentials.api_key).toBe('[__HIDDEN__]')
+ expect(mockNotify).toHaveBeenCalled()
+ expect(mockHandleRefreshModel).toHaveBeenCalled()
+ expect(onSave).toHaveBeenCalledWith('test-provider')
+ expect(onClose).toHaveBeenCalled()
+ })
+ })
+
+ it('should close modal when switching credential yields no available credentials', async () => {
+ const onClose = vi.fn()
+ mockRefetch.mockResolvedValue({ data: { available_credentials: [] } })
+
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByRole('button', { name: 'switch credential' }))
+ await waitFor(() => {
+ expect(onClose).toHaveBeenCalled()
+ })
+ })
+
+ it('should confirm model deletion and close modal', async () => {
+ const onClose = vi.fn()
+ mockDeleteModel = { model: 'gpt-4' }
+
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByText(/modelProvider\.auth\.removeModel/))
+ fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
+
+ await waitFor(() => {
+ expect(mockOpenConfirmDelete).toHaveBeenCalled()
+ expect(mockHandleConfirmDelete).toHaveBeenCalled()
+ expect(onClose).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-selector.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-selector.spec.tsx
new file mode 100644
index 0000000000..3d4dc24a79
--- /dev/null
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-selector.spec.tsx
@@ -0,0 +1,29 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import PrioritySelector from './priority-selector'
+
+describe('PrioritySelector', () => {
+ const mockOnSelect = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('should render selector button', () => {
+ render()
+ expect(screen.getByRole('button')).toBeInTheDocument()
+ })
+
+ it('should call onSelect when option clicked', () => {
+ render()
+ fireEvent.click(screen.getByRole('button'))
+ const option = screen.getByText('common.modelProvider.apiKey')
+ fireEvent.click(option)
+ expect(mockOnSelect).toHaveBeenCalled()
+ })
+
+ it('should display priority use header in popover', () => {
+ render()
+ fireEvent.click(screen.getByRole('button'))
+ expect(screen.getByText('common.modelProvider.card.priorityUse')).toBeInTheDocument()
+ })
+})
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.spec.tsx
new file mode 100644
index 0000000000..86e51c4a53
--- /dev/null
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.spec.tsx
@@ -0,0 +1,14 @@
+import { render } from '@testing-library/react'
+import PriorityUseTip from './priority-use-tip'
+
+describe('PriorityUseTip', () => {
+ it('should render tooltip with icon content', () => {
+ const { container } = render()
+ expect(container.querySelector('[data-state]')).toBeInTheDocument()
+ })
+
+ it('should render the component without crashing', () => {
+ const { container } = render()
+ expect(container.firstChild).toBeInTheDocument()
+ })
+})
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.spec.tsx
new file mode 100644
index 0000000000..1088114a59
--- /dev/null
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.spec.tsx
@@ -0,0 +1,138 @@
+import type { ModelProvider } from '../declarations'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import QuotaPanel from './quota-panel'
+
+let mockWorkspace = {
+ trial_credits: 100,
+ trial_credits_used: 30,
+ next_credit_reset_date: '2024-12-31',
+}
+let mockTrialModels: string[] = ['langgenius/openai/openai']
+let mockPlugins = [{
+ plugin_id: 'langgenius/openai',
+ latest_package_identifier: 'openai@1.0.0',
+}]
+
+vi.mock('@/app/components/base/icons/src/public/llm', () => {
+ const Icon = ({ label }: { label: string }) => {label}
+ return {
+ OpenaiSmall: () => ,
+ AnthropicShortLight: () => ,
+ Gemini: () => ,
+ Grok: () => ,
+ Deepseek: () => ,
+ Tongyi: () => ,
+ }
+})
+
+vi.mock('@/context/app-context', () => ({
+ useAppContext: () => ({
+ currentWorkspace: mockWorkspace,
+ }),
+}))
+
+vi.mock('@/context/global-public-context', () => ({
+ useGlobalPublicStore: (selector: (state: { systemFeatures: { trial_models: string[] } }) => unknown) => selector({
+ systemFeatures: {
+ trial_models: mockTrialModels,
+ },
+ }),
+}))
+
+vi.mock('../hooks', () => ({
+ useMarketplaceAllPlugins: () => ({
+ plugins: mockPlugins,
+ }),
+}))
+
+vi.mock('@/hooks/use-timestamp', () => ({
+ default: () => ({
+ formatTime: () => '2024-12-31',
+ }),
+}))
+
+vi.mock('@/app/components/plugins/install-plugin/install-from-marketplace', () => ({
+ default: ({ onClose }: { onClose: () => void }) => (
+
+ install modal
+
+
+ ),
+}))
+
+describe('QuotaPanel', () => {
+ const mockProviders = [
+ {
+ provider: 'langgenius/openai/openai',
+ preferred_provider_type: 'custom',
+ custom_configuration: { available_credentials: [{ id: '1' }] },
+ },
+ ] as unknown as ModelProvider[]
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockWorkspace = {
+ trial_credits: 100,
+ trial_credits_used: 30,
+ next_credit_reset_date: '2024-12-31',
+ }
+ mockTrialModels = ['langgenius/openai/openai']
+ mockPlugins = [{ plugin_id: 'langgenius/openai', latest_package_identifier: 'openai@1.0.0' }]
+ })
+
+ it('should render loading state', () => {
+ render(
+ ,
+ )
+ expect(screen.getByRole('status')).toBeInTheDocument()
+ })
+
+ it('should show remaining credits and reset date', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText(/modelProvider\.quota/)).toBeInTheDocument()
+ expect(screen.getByText('70')).toBeInTheDocument()
+ expect(screen.getByText(/modelProvider\.resetDate/)).toBeInTheDocument()
+ })
+
+ it('should floor credits at zero when usage is higher than quota', () => {
+ mockWorkspace = {
+ trial_credits: 10,
+ trial_credits_used: 999,
+ next_credit_reset_date: '',
+ }
+
+ render()
+
+ expect(screen.getByText('0')).toBeInTheDocument()
+ expect(screen.queryByText(/modelProvider\.resetDate/)).not.toBeInTheDocument()
+ })
+
+ it('should open install modal when clicking an unsupported trial provider', () => {
+ render()
+
+ fireEvent.click(screen.getByText('openai'))
+
+ expect(screen.getByText('install modal')).toBeInTheDocument()
+ })
+
+ it('should close install modal when provider becomes installed', async () => {
+ const { rerender } = render()
+
+ fireEvent.click(screen.getByText('openai'))
+ expect(screen.getByText('install modal')).toBeInTheDocument()
+
+ rerender()
+
+ await waitFor(() => {
+ expect(screen.queryByText('install modal')).not.toBeInTheDocument()
+ })
+ })
+})