agent-ecosystem/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts

659 lines
22 KiB
TypeScript

import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { RuntimeProviderManagementPanelView } from '../../../../src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView';
import type {
RuntimeProviderManagementActions,
RuntimeProviderManagementState,
} from '../../../../src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement';
function createState(
overrides: Partial<RuntimeProviderManagementState> = {}
): RuntimeProviderManagementState {
return {
view: {
runtimeId: 'opencode',
title: 'OpenCode',
runtime: {
state: 'ready',
cliPath: '/usr/local/bin/opencode',
version: '1.14.24',
managedProfile: 'active',
localAuth: 'synced',
},
providers: [
{
providerId: 'openrouter',
displayName: 'OpenRouter',
state: 'available',
ownership: [],
recommended: true,
modelCount: 4,
defaultModelId: null,
authMethods: ['api'],
actions: [
{
id: 'connect',
label: 'Connect',
enabled: true,
disabledReason: null,
requiresSecret: true,
ownershipScope: 'managed',
},
],
detail: null,
},
],
defaultModel: null,
fallbackModel: null,
diagnostics: [],
},
providers: [],
selectedProviderId: 'openrouter',
providerQuery: '',
directoryOpen: false,
directoryLoading: false,
directoryRefreshing: false,
directoryError: null,
directoryEntries: [],
directoryTotalCount: null,
directoryNextCursor: null,
directoryQuery: '',
directoryFilter: 'all',
directoryLoaded: false,
directorySelectedProviderId: null,
directorySupported: true,
activeFormProviderId: null,
apiKeyValue: '',
modelPickerProviderId: null,
modelPickerMode: null,
modelQuery: '',
models: [],
modelsLoading: false,
modelsError: null,
selectedModelId: null,
testingModelId: null,
savingDefaultModelId: null,
modelResults: {},
loading: false,
savingProviderId: null,
error: null,
successMessage: null,
...overrides,
};
}
function createActions(): RuntimeProviderManagementActions {
return {
refresh: vi.fn(() => Promise.resolve()),
selectProvider: vi.fn(),
setProviderQuery: vi.fn(),
openDirectory: vi.fn(),
closeDirectory: vi.fn(),
setDirectoryQuery: vi.fn(),
setDirectoryFilter: vi.fn(),
loadMoreDirectory: vi.fn(() => Promise.resolve()),
refreshDirectory: vi.fn(() => Promise.resolve()),
selectDirectoryProvider: vi.fn(),
searchAllProviders: vi.fn(),
startConnect: vi.fn(),
cancelConnect: vi.fn(),
setApiKeyValue: vi.fn(),
submitConnect: vi.fn(() => Promise.resolve()),
forgetProvider: vi.fn(() => Promise.resolve()),
openModelPicker: vi.fn(),
closeModelPicker: vi.fn(),
setModelQuery: vi.fn(),
selectModel: vi.fn(),
useModelForNewTeams: vi.fn(),
testModel: vi.fn(() => Promise.resolve()),
setDefaultModel: vi.fn(() => Promise.resolve()),
};
}
describe('RuntimeProviderManagementPanelView', () => {
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
});
afterEach(() => {
document.body.innerHTML = '';
vi.unstubAllGlobals();
});
it('renders an explicit loading state while the managed OpenCode view is loading', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: createState({
view: null,
providers: [],
loading: true,
}),
actions,
disabled: false,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('Checking runtime');
expect(host.textContent).toContain('Loading managed OpenCode runtime');
expect(host.textContent).toContain('Loading OpenCode providers');
expect(host.querySelector('[data-testid="runtime-provider-loading-skeleton"]')).not.toBeNull();
expect(host.querySelectorAll('.skeleton-shimmer').length).toBeGreaterThanOrEqual(10);
expect(host.textContent).toContain('Checking...');
const refreshButton = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Checking...')
);
expect(refreshButton?.disabled).toBe(true);
});
it('renders provider actions and opens API-key form state without exposing a raw secret', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
const state = createState();
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: { ...state, providers: state.view?.providers ?? [] },
actions,
disabled: false,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('OpenRouter');
expect(host.textContent).toContain('4 models');
expect(host.querySelector('[data-testid="runtime-provider-search"]')).not.toBeNull();
expect(
host.querySelector('[data-testid="runtime-provider-row-openrouter"]')?.className
).toContain('hover:bg-sky-400');
await act(async () => {
host
.querySelector('[data-testid="runtime-provider-row-openrouter"]')
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(actions.selectProvider).toHaveBeenCalledWith('openrouter');
await act(async () => {
const connect = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Connect')
);
connect?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(actions.startConnect).toHaveBeenCalledWith('openrouter');
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: {
...state,
providers: state.view?.providers ?? [],
activeFormProviderId: 'openrouter',
apiKeyValue: 'sk-secret-value',
},
actions,
disabled: false,
})
);
await Promise.resolve();
});
expect(host.querySelector('input[type="password"]')).not.toBeNull();
expect(host.textContent).not.toContain('sk-secret-value');
});
it('filters providers from the local provider search', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
const openRouterProvider = createState().view!.providers[0];
const openAiProvider = {
...openRouterProvider,
providerId: 'openai',
displayName: 'OpenAI',
recommended: false,
};
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: createState({
view: {
...createState().view!,
providers: [openRouterProvider, openAiProvider],
},
providers: [openRouterProvider, openAiProvider],
providerQuery: 'router',
}),
actions,
disabled: false,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('OpenRouter');
expect(host.textContent).not.toContain('OpenAI');
expect(host.querySelector('[data-testid="runtime-provider-search"]')).not.toBeNull();
});
it('opens the OpenCode provider directory and renders directory rows', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: createState({
directoryOpen: true,
directoryLoaded: true,
directoryTotalCount: 115,
directoryEntries: [
{
providerId: 'deepseek',
displayName: 'DeepSeek',
state: 'available',
setupKind: 'available-readonly',
ownership: [],
recommended: false,
modelCount: 62,
defaultModelId: null,
authMethods: [],
actions: [
{
id: 'configure',
label: 'Configure manually',
enabled: false,
disabledReason: 'OpenCode did not advertise API-key auth',
requiresSecret: false,
ownershipScope: 'runtime',
},
],
sources: ['opencode-provider'],
sourceLabel: 'OpenCode catalog',
providerSource: 'models.dev',
detail: 'Models are visible, but no connected credential was reported',
metadata: {
hasKnownModels: true,
requiresManualConfig: false,
supportedInlineAuth: false,
},
},
],
}),
actions,
disabled: false,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('115 OpenCode providers');
expect(host.textContent).toContain('DeepSeek');
expect(host.textContent).toContain('62 models');
expect(host.textContent).toContain('OpenCode catalog');
expect(host.querySelector('[data-testid="runtime-provider-directory-search"]')).not.toBeNull();
await act(async () => {
host
.querySelector('[data-testid="runtime-provider-directory-row-deepseek"]')
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(actions.selectDirectoryProvider).toHaveBeenCalledWith('deepseek');
});
it('offers global provider search when compact search has no matches', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
const state = createState();
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: {
...state,
providers: state.view?.providers ?? [],
providerQuery: 'deep',
},
actions,
disabled: false,
})
);
await Promise.resolve();
});
const searchAll = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Search all OpenCode providers for "deep"')
);
expect(searchAll).not.toBeNull();
await act(async () => {
searchAll?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(actions.searchAllProviders).toHaveBeenCalledWith('deep');
});
it('renders connected provider model picker actions', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
const connectedProvider = {
providerId: 'openrouter',
displayName: 'OpenRouter',
state: 'connected' as const,
ownership: ['managed'] as const,
recommended: true,
modelCount: 174,
defaultModelId: null,
authMethods: ['api'] as const,
actions: [
{
id: 'use' as const,
label: 'Use',
enabled: true,
disabledReason: null,
requiresSecret: false,
ownershipScope: 'runtime' as const,
},
{
id: 'set-default' as const,
label: 'Set default',
enabled: true,
disabledReason: null,
requiresSecret: false,
ownershipScope: 'runtime' as const,
},
],
detail: null,
};
const state = createState({
view: {
...createState().view!,
providers: [connectedProvider],
},
providers: [connectedProvider],
modelPickerProviderId: 'openrouter',
modelPickerMode: 'use',
models: [
{
providerId: 'openrouter',
modelId: 'openrouter/openai/gpt-oss-20b:free',
displayName: 'openai/gpt-oss-20b:free',
sourceLabel: 'OpenRouter',
free: true,
default: false,
availability: 'untested',
},
{
providerId: 'openrouter',
modelId: 'opencode/big-pickle',
displayName: 'opencode/big-pickle',
sourceLabel: 'OpenCode',
free: false,
default: false,
availability: 'untested',
},
{
providerId: 'openrouter',
modelId: 'openrouter/qwen/qwen3-coder-flash',
displayName: 'qwen/qwen3-coder-flash',
sourceLabel: 'OpenRouter',
free: false,
default: false,
availability: 'untested',
},
],
selectedModelId: 'openrouter/openai/gpt-oss-20b:free',
modelResults: {
'openrouter/openai/gpt-oss-20b:free': {
providerId: 'openrouter',
modelId: 'openrouter/openai/gpt-oss-20b:free',
ok: true,
availability: 'available',
message: 'Model probe passed',
diagnostics: [],
},
},
});
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state,
actions,
disabled: false,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('openrouter/openai/gpt-oss-20b:free');
expect(host.textContent).toContain('Used for new teams');
expect(host.textContent).toContain('Model probe passed');
expect(host.textContent).toContain('Not recommended');
expect(host.textContent).toContain('Recommended only');
expect(host.textContent).toContain('Recommended');
expect(host.textContent).not.toContain('Set OpenCode default');
expect(
Array.from(host.querySelectorAll('button')).some(
(button) => button.textContent?.trim() === 'Use for new teams'
)
).toBe(false);
expect(
host.querySelector('[data-testid="runtime-provider-logo-openrouter"] svg')
).not.toBeNull();
const connectedBadge = Array.from(host.querySelectorAll('span')).find(
(span) => span.textContent === 'Connected'
) as HTMLElement | undefined;
expect(connectedBadge?.style.color).toBeTruthy();
expect(
(
host.querySelector(
'[data-testid="runtime-provider-model-search"]'
) as HTMLElement | null
)?.style.paddingLeft
).toBe('42px');
expect(
(host.querySelector('[data-testid="runtime-provider-model-list"]') as HTMLElement | null)
?.style.maxHeight
).toBe('300px');
expect(host.textContent).not.toContain('OpenRouterfree');
const firstTestButton = Array.from(host.querySelectorAll('button')).find(
(button) => button.textContent?.trim() === 'Test'
);
expect(firstTestButton?.className).toContain('border');
const modelResult = host.querySelector(
'[data-testid="runtime-provider-model-result-openrouter/openai/gpt-oss-20b:free"]'
) as HTMLElement | null;
expect(modelResult?.style.color).toBe('#86efac');
expect((host.textContent ?? '').indexOf('qwen/qwen3-coder-flash')).toBeLessThan(
(host.textContent ?? '').indexOf('opencode/big-pickle')
);
expect((host.textContent ?? '').indexOf('opencode/big-pickle')).toBeLessThan(
(host.textContent ?? '').indexOf('openrouter/openai/gpt-oss-20b:free')
);
await act(async () => {
const checkbox = Array.from(host.querySelectorAll('button')).find(
(button) => button.getAttribute('role') === 'checkbox'
);
checkbox?.click();
await Promise.resolve();
});
expect(host.textContent).toContain('qwen/qwen3-coder-flash');
expect(host.textContent).not.toContain('opencode/big-pickle');
expect(host.textContent).not.toContain('openrouter/openai/gpt-oss-20b:free');
await act(async () => {
const checkbox = Array.from(host.querySelectorAll('button')).find(
(button) => button.getAttribute('role') === 'checkbox'
);
checkbox?.click();
await Promise.resolve();
});
await act(async () => {
host
.querySelector(
'[data-testid="runtime-provider-model-row-openrouter/openai/gpt-oss-20b:free"]'
)
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(actions.useModelForNewTeams).toHaveBeenCalledWith('openrouter/openai/gpt-oss-20b:free');
vi.mocked(actions.useModelForNewTeams).mockClear();
await act(async () => {
const notRecommendedRow = host.querySelector(
'[data-testid="runtime-provider-model-row-openrouter/openai/gpt-oss-20b:free"]'
);
const notRecommendedTestButton = Array.from(
notRecommendedRow?.querySelectorAll('button') ?? []
).find((button) => button.textContent?.trim() === 'Test');
notRecommendedTestButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(actions.testModel).toHaveBeenCalledWith(
'openrouter',
'openrouter/openai/gpt-oss-20b:free'
);
expect(actions.useModelForNewTeams).not.toHaveBeenCalled();
});
it('renders verified brand icons for common OpenCode providers', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
const baseProvider = createState().view!.providers[0];
const providers = [
{ providerId: 'openrouter', displayName: 'OpenRouter' },
{ providerId: 'opencode', displayName: 'OpenCode Zen' },
{ providerId: 'openai', displayName: 'OpenAI' },
{ providerId: 'anthropic', displayName: 'Anthropic' },
{ providerId: 'google', displayName: 'Google' },
{ providerId: 'google-vertex', displayName: 'Vertex' },
{ providerId: 'vercel', displayName: 'Vercel AI Gateway' },
{ providerId: 'mistral', displayName: 'Mistral' },
{ providerId: 'github-models', displayName: 'GitHub Models' },
{ providerId: 'perplexity-agent', displayName: 'Perplexity Agent' },
{ providerId: 'nvidia', displayName: 'Nvidia' },
{ providerId: 'minimax', displayName: 'MiniMax' },
{ providerId: 'cloudflare-ai-gateway', displayName: 'Cloudflare AI Gateway' },
{ providerId: 'cloudflare-workers-ai', displayName: 'Cloudflare Workers AI' },
{ providerId: 'gitlab-duo', displayName: 'GitLab Duo' },
{ providerId: 'poe', displayName: 'Poe' },
].map((provider) => ({
...baseProvider,
...provider,
state: 'not-connected' as const,
recommended: false,
}));
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: createState({
view: {
...createState().view!,
providers,
},
providers,
}),
actions,
disabled: false,
})
);
await Promise.resolve();
});
for (const provider of providers) {
const logo = host.querySelector(
`[data-testid="runtime-provider-logo-${provider.providerId}"]`
);
expect(logo).not.toBeNull();
expect(logo?.querySelector('svg,img')).not.toBeNull();
}
});
it('uses branded initials for popular providers without verified compact logo assets', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const actions = createActions();
const baseProvider = createState().view!.providers[0];
const providers = [
{ providerId: 'xai', displayName: 'xAI', label: 'xAI' },
{ providerId: 'groq', displayName: 'Groq', label: 'G' },
{ providerId: 'deepseek', displayName: 'DeepSeek', label: 'DS' },
{ providerId: 'deepinfra', displayName: 'Deep Infra', label: 'DI' },
{ providerId: 'fireworks-ai', displayName: 'Fireworks AI', label: 'FW' },
{ providerId: 'togetherai', displayName: 'Together AI', label: 'TA' },
{ providerId: 'amazon-bedrock', displayName: 'Amazon Bedrock', label: 'AWS' },
{ providerId: 'azure', displayName: 'Azure', label: 'AZ' },
{ providerId: 'cohere', displayName: 'Cohere', label: 'CO' },
{ providerId: 'ollama-cloud', displayName: 'Ollama Cloud', label: 'OL' },
].map((provider) => ({
...baseProvider,
...provider,
state: 'not-connected' as const,
recommended: false,
}));
await act(async () => {
root.render(
React.createElement(RuntimeProviderManagementPanelView, {
state: createState({
view: {
...createState().view!,
providers,
},
providers,
}),
actions,
disabled: false,
})
);
await Promise.resolve();
});
for (const provider of providers) {
const logo = host.querySelector(
`[data-testid="runtime-provider-logo-${provider.providerId}"]`
);
expect(logo?.textContent).toBe(provider.label);
}
});
});