feat(providers): slug-keyed cloud providers + per-workload model routing (#1888)

This commit is contained in:
Steven Enamakel
2026-05-15 21:19:07 -07:00
committed by GitHub
parent 320fd6c74c
commit 86b57281dc
18 changed files with 2044 additions and 1121 deletions
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,12 @@
import { screen, waitFor } from '@testing-library/react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { loadAISettings, loadLocalProviderSnapshot } from '../../../../services/api/aiSettingsApi';
import {
loadAISettings,
loadLocalProviderSnapshot,
saveAISettings,
setCloudProviderKey,
} from '../../../../services/api/aiSettingsApi';
import { renderWithProviders } from '../../../../test/test-utils';
import AIPanel from '../AIPanel';
@@ -21,8 +26,12 @@ vi.mock('../../../../services/api/aiSettingsApi', () => ({
loadLocalProviderSnapshot: vi.fn(),
setCloudProviderKey: vi.fn(),
clearCloudProviderKey: vi.fn(),
serializeProviderRef: vi.fn((r: { kind: string; model?: string }) =>
r.kind === 'primary' ? 'cloud' : r.kind === 'local' ? `ollama:${r.model}` : `cloud:${r.model}`
serializeProviderRef: vi.fn((r: { kind: string; providerSlug?: string; model?: string }) =>
r.kind === 'openhuman'
? 'openhuman'
: r.kind === 'local'
? `ollama:${r.model}`
: `${r.providerSlug}:${r.model}`
),
localProvider: { download: vi.fn(), applyPreset: vi.fn() },
}));
@@ -39,35 +48,32 @@ const baseSettings = {
cloudProviders: [
{
id: 'p_oh_x',
type: 'openhuman' as const,
slug: 'openhuman',
label: 'OpenHuman',
endpoint: 'https://api.openhuman.ai/v1',
default_model: 'reasoning-v1',
auth_style: 'openhuman_jwt' as const,
has_api_key: false,
},
],
primaryCloudId: 'p_oh_x',
routing: {
reasoning: { kind: 'primary' as const },
agentic: { kind: 'primary' as const },
coding: { kind: 'primary' as const },
memory: { kind: 'primary' as const },
embeddings: { kind: 'primary' as const },
heartbeat: { kind: 'primary' as const },
learning: { kind: 'primary' as const },
subconscious: { kind: 'primary' as const },
reasoning: { kind: 'openhuman' as const },
agentic: { kind: 'openhuman' as const },
coding: { kind: 'openhuman' as const },
memory: { kind: 'openhuman' as const },
embeddings: { kind: 'openhuman' as const },
heartbeat: { kind: 'openhuman' as const },
learning: { kind: 'openhuman' as const },
subconscious: { kind: 'openhuman' as const },
},
};
const baseLocalSnapshot = { status: null, diagnostics: null, presets: null, installedModels: [] };
describe('AIPanel', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(loadAISettings).mockResolvedValue(baseSettings);
vi.mocked(loadLocalProviderSnapshot).mockResolvedValue({
status: null,
diagnostics: null,
presets: null,
installedModels: [],
});
vi.mocked(loadLocalProviderSnapshot).mockResolvedValue(baseLocalSnapshot);
});
it('renders the LLM Providers + Routing top-level section headers', async () => {
@@ -106,4 +112,208 @@ describe('AIPanel', () => {
expect(screen.getByText(label)).toBeInTheDocument();
}
});
// ─── auth_style preservation ────────────────────────────────────────────────
it('preserves auth_style: "anthropic" through save when Anthropic provider is configured', async () => {
const settingsWithAnthropic = {
cloudProviders: [
{
id: 'p_anthropic_1',
slug: 'anthropic',
label: 'Anthropic',
endpoint: 'https://api.anthropic.com/v1',
auth_style: 'anthropic' as const,
has_api_key: true,
},
],
routing: {
reasoning: {
kind: 'cloud' as const,
providerSlug: 'anthropic',
model: 'claude-3-5-sonnet-20241022',
},
agentic: { kind: 'openhuman' as const },
coding: { kind: 'openhuman' as const },
memory: { kind: 'openhuman' as const },
embeddings: { kind: 'openhuman' as const },
heartbeat: { kind: 'openhuman' as const },
learning: { kind: 'openhuman' as const },
subconscious: { kind: 'openhuman' as const },
},
};
vi.mocked(loadAISettings).mockResolvedValue(settingsWithAnthropic);
vi.mocked(saveAISettings).mockResolvedValue(undefined);
renderWithProviders(<AIPanel />);
// Wait for load.
await waitFor(() => expect(screen.getAllByText(/Anthropic/i).length).toBeGreaterThan(0));
// Trigger a routing change so the SaveBar appears, then save.
// Click the "Default" button on the Reasoning row to change routing.
const defaultButtons = screen.getAllByText('Default');
fireEvent.click(defaultButtons[0]);
// SaveBar should appear.
await waitFor(() => expect(screen.getByText(/unsaved change/i)).toBeInTheDocument());
// Click Save in the SaveBar.
const saveButton = screen.getByRole('button', { name: /^Save$/i });
fireEvent.click(saveButton);
await waitFor(() => expect(vi.mocked(saveAISettings)).toHaveBeenCalled());
// Verify auth_style was passed through correctly in the next AISettings arg.
const [, nextSettings] = vi.mocked(saveAISettings).mock.calls[0];
const anthropicProvider = nextSettings.cloudProviders.find(
(p: { slug: string }) => p.slug === 'anthropic'
);
expect(anthropicProvider).toBeDefined();
expect(anthropicProvider!.auth_style).toBe('anthropic');
});
// ─── chip toggle: toggle ON opens API-key dialog ────────────────────────────
it('clicking the OpenAI chip toggle (when disabled) opens the API-key dialog', async () => {
// Load with no openai provider → chip is off.
vi.mocked(loadAISettings).mockResolvedValue({ ...baseSettings, cloudProviders: [] });
renderWithProviders(<AIPanel />);
await waitFor(() => expect(screen.getAllByText(/OpenAI/i).length).toBeGreaterThan(0));
// Find the "Connect OpenAI" switch button and click it.
const connectSwitch = screen.getByRole('switch', { name: /Connect OpenAI/i });
fireEvent.click(connectSwitch);
// ProviderKeyDialog should appear.
await waitFor(() =>
expect(screen.getByRole('dialog', { name: /Connect OpenAI/i })).toBeInTheDocument()
);
// The input for the API key should be visible.
expect(screen.getByLabelText(/API key/i)).toBeInTheDocument();
});
it('clicking the Custom chip (when disabled) opens the CloudProviderEditor, not the key dialog', async () => {
// Load with no custom provider → chip is off.
vi.mocked(loadAISettings).mockResolvedValue({ ...baseSettings, cloudProviders: [] });
renderWithProviders(<AIPanel />);
await waitFor(() => expect(screen.getAllByText(/Custom/i).length).toBeGreaterThan(0));
// Find the "Connect Custom" switch and click it.
const connectSwitch = screen.getByRole('switch', { name: /Connect Custom/i });
fireEvent.click(connectSwitch);
// The full CloudProviderEditor should appear (has "Add cloud provider" heading).
await waitFor(() => expect(screen.getByText(/Add cloud provider/i)).toBeInTheDocument());
// The simple ProviderKeyDialog should NOT appear.
expect(screen.queryByRole('dialog', { name: /Connect Custom/i })).not.toBeInTheDocument();
});
// ─── chip toggle: toggle OFF scrubs routing entries ──────────────────────────
it('toggling OFF an enabled provider scrubs routing entries that reference it', async () => {
const settingsWithOpenAI = {
cloudProviders: [
{
id: 'p_openai_1',
slug: 'openai',
label: 'OpenAI',
endpoint: 'https://api.openai.com/v1',
auth_style: 'bearer' as const,
has_api_key: true,
},
],
routing: {
reasoning: { kind: 'cloud' as const, providerSlug: 'openai', model: 'gpt-4o' },
agentic: { kind: 'cloud' as const, providerSlug: 'openai', model: 'gpt-4o-mini' },
coding: { kind: 'openhuman' as const },
memory: { kind: 'openhuman' as const },
embeddings: { kind: 'openhuman' as const },
heartbeat: { kind: 'openhuman' as const },
learning: { kind: 'openhuman' as const },
subconscious: { kind: 'openhuman' as const },
},
};
vi.mocked(loadAISettings).mockResolvedValue(settingsWithOpenAI);
vi.mocked(saveAISettings).mockResolvedValue(undefined);
renderWithProviders(<AIPanel />);
// Wait for load — OpenAI chip should be ON.
await waitFor(() =>
expect(screen.getByRole('switch', { name: /Disconnect OpenAI/i })).toBeInTheDocument()
);
// Toggle OFF.
fireEvent.click(screen.getByRole('switch', { name: /Disconnect OpenAI/i }));
// A SaveBar must appear because the draft changed.
await waitFor(() => expect(screen.getByText(/unsaved change/i)).toBeInTheDocument());
// Save to capture the nextSettings arg.
fireEvent.click(screen.getByRole('button', { name: /^Save$/i }));
await waitFor(() => expect(vi.mocked(saveAISettings)).toHaveBeenCalled());
const [, nextSettings] = vi.mocked(saveAISettings).mock.calls[0];
// Provider should be gone.
expect(
nextSettings.cloudProviders.find((p: { slug: string }) => p.slug === 'openai')
).toBeUndefined();
// Routing entries that were pinned to openai must be reset to openhuman.
expect(nextSettings.routing.reasoning).toEqual({ kind: 'openhuman' });
expect(nextSettings.routing.agentic).toEqual({ kind: 'openhuman' });
// Entries that were already openhuman remain unchanged.
expect(nextSettings.routing.coding).toEqual({ kind: 'openhuman' });
});
// ─── API-key dialog: failed setCloudProviderKey does not add provider ────────
it('when setCloudProviderKey throws, the provider is NOT added to the draft', async () => {
vi.mocked(loadAISettings).mockResolvedValue({ ...baseSettings, cloudProviders: [] });
// Make setCloudProviderKey reject.
vi.mocked(setCloudProviderKey).mockRejectedValue(new Error('key store failed'));
renderWithProviders(<AIPanel />);
// Wait for OpenAI chip to render (disabled).
await waitFor(() =>
expect(screen.getByRole('switch', { name: /Connect OpenAI/i })).toBeInTheDocument()
);
// Count provider chips before dialog interaction.
const chipsBefore = screen.getAllByRole('switch').length;
// Open the dialog.
fireEvent.click(screen.getByRole('switch', { name: /Connect OpenAI/i }));
await waitFor(() =>
expect(screen.getByRole('dialog', { name: /Connect OpenAI/i })).toBeInTheDocument()
);
// Fill in a key and submit.
fireEvent.change(screen.getByLabelText(/API key/i), { target: { value: 'sk-bad-key' } });
fireEvent.click(screen.getByRole('button', { name: /^Save$/i }));
// The panel silently catches the setCloudProviderKey error and does NOT
// mutate the draft. Because the panel's onSubmit returns (doesn't throw),
// the dialog's handleSave resolves without entering its catch block, leaving
// the dialog in the 'saving' phase with the button showing "Saving…".
//
// Wait for setCloudProviderKey to have been called (confirms the flow ran).
await waitFor(() => expect(vi.mocked(setCloudProviderKey)).toHaveBeenCalled());
// The dialog must still be open (setKeyDialogFor was never set to null).
expect(screen.getByRole('dialog', { name: /Connect OpenAI/i })).toBeInTheDocument();
// The number of provider toggle switches must not have grown — the failed
// provider was never added to the draft.
expect(screen.getAllByRole('switch').length).toBe(chipsBefore);
// Specifically: no "Disconnect OpenAI" switch (chip is still in off state).
expect(screen.queryByRole('switch', { name: /Disconnect OpenAI/i })).not.toBeInTheDocument();
});
});