1302 lines
42 KiB
TypeScript
1302 lines
42 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: '',
|
|
directoryLoading: false,
|
|
directoryRefreshing: false,
|
|
directoryError: null,
|
|
directoryEntries: [],
|
|
directoryTotalCount: null,
|
|
directoryNextCursor: null,
|
|
directoryLoaded: false,
|
|
directorySelectedProviderId: null,
|
|
directorySupported: true,
|
|
activeFormProviderId: null,
|
|
setupForm: null,
|
|
setupFormLoading: false,
|
|
setupFormError: null,
|
|
setupSubmitError: null,
|
|
setupMetadata: {},
|
|
apiKeyValue: '',
|
|
modelPickerProviderId: null,
|
|
modelPickerMode: null,
|
|
modelQuery: '',
|
|
models: [],
|
|
modelsLoading: false,
|
|
modelsError: null,
|
|
selectedModelId: null,
|
|
testingModelIds: [],
|
|
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(),
|
|
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(),
|
|
setSetupMetadataValue: 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.startConnect).toHaveBeenCalledWith('openrouter');
|
|
expect(actions.selectProvider).not.toHaveBeenCalled();
|
|
|
|
vi.mocked(actions.startConnect).mockClear();
|
|
|
|
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',
|
|
setupForm: {
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
displayName: 'OpenRouter',
|
|
method: 'api',
|
|
supported: true,
|
|
title: 'Connect OpenRouter',
|
|
description: null,
|
|
submitLabel: 'Connect',
|
|
disabledReason: null,
|
|
source: 'curated',
|
|
secret: {
|
|
key: 'key',
|
|
label: 'API key',
|
|
placeholder: 'Paste API key',
|
|
required: true,
|
|
},
|
|
prompts: [],
|
|
},
|
|
},
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.querySelector('input[type="password"]')).not.toBeNull();
|
|
expect(host.textContent).not.toContain('sk-secret-value');
|
|
});
|
|
|
|
it('allows supported setup forms that do not require a secret to submit', 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 ?? [],
|
|
activeFormProviderId: 'openrouter',
|
|
setupForm: {
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
displayName: 'OpenRouter',
|
|
method: 'oauth',
|
|
supported: true,
|
|
title: 'Connect OpenRouter',
|
|
description: null,
|
|
submitLabel: 'Connect',
|
|
disabledReason: null,
|
|
source: 'oauth',
|
|
secret: null,
|
|
prompts: [],
|
|
},
|
|
},
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const submitButton = Array.from(host.querySelectorAll('button'))
|
|
.filter((button) => button.textContent?.trim() === 'Connect')
|
|
.at(-1);
|
|
expect(submitButton?.disabled).toBe(false);
|
|
});
|
|
|
|
it('renders multiple compact provider actions without hiding forget behind connect', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const provider = {
|
|
...createState().view!.providers[0],
|
|
actions: [
|
|
{
|
|
id: 'connect' as const,
|
|
label: 'Connect',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: true,
|
|
ownershipScope: 'managed' as const,
|
|
},
|
|
{
|
|
id: 'forget' as const,
|
|
label: 'Forget',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: false,
|
|
ownershipScope: 'managed' as const,
|
|
},
|
|
],
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
providers: [provider],
|
|
},
|
|
providers: [provider],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const buttons = Array.from(host.querySelectorAll('button'));
|
|
expect(buttons.some((button) => button.textContent?.includes('Connect'))).toBe(true);
|
|
expect(buttons.some((button) => button.textContent?.includes('Forget'))).toBe(true);
|
|
|
|
await act(async () => {
|
|
buttons
|
|
.find((button) => button.textContent?.includes('Forget'))
|
|
?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.startConnect).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
buttons
|
|
.find((button) => button.textContent?.includes('Forget'))
|
|
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.forgetProvider).toHaveBeenCalledWith('openrouter');
|
|
expect(actions.startConnect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('supports keyboard activation for compact provider rows', 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();
|
|
});
|
|
|
|
const row = host.querySelector('[data-testid="runtime-provider-row-openrouter"]');
|
|
expect(row?.getAttribute('role')).toBe('button');
|
|
expect(row?.getAttribute('tabindex')).toBe('0');
|
|
|
|
await act(async () => {
|
|
row?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.startConnect).toHaveBeenCalledWith('openrouter');
|
|
});
|
|
|
|
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('does not open a model list for a render-only filtered fallback provider', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const openRouterProvider = {
|
|
...createState().view!.providers[0],
|
|
state: 'connected' as const,
|
|
modelCount: 174,
|
|
actions: [],
|
|
};
|
|
const openAiProvider = {
|
|
...openRouterProvider,
|
|
providerId: 'openai',
|
|
displayName: 'OpenAI',
|
|
recommended: false,
|
|
defaultModelId: 'openai/gpt-5.4-mini-fast',
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
providers: [openRouterProvider, openAiProvider],
|
|
},
|
|
providers: [openRouterProvider, openAiProvider],
|
|
selectedProviderId: 'openrouter',
|
|
modelPickerProviderId: 'openrouter',
|
|
modelPickerMode: 'use',
|
|
providerQuery: 'openai',
|
|
models: [
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/openai/gpt-oss-20b:free',
|
|
displayName: 'openai/gpt-oss-20b:free',
|
|
sourceLabel: 'OpenRouter',
|
|
free: true,
|
|
default: false,
|
|
availability: 'untested',
|
|
},
|
|
],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('OpenAI');
|
|
expect(host.textContent).not.toContain('OpenRouter');
|
|
expect(
|
|
host.querySelector('[data-testid="runtime-provider-model-loading-skeleton"]')
|
|
).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({
|
|
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,
|
|
},
|
|
},
|
|
{
|
|
providerId: 'cloudflare-workers-ai',
|
|
displayName: 'Cloudflare Workers AI',
|
|
state: 'not-connected',
|
|
setupKind: 'connect-api-key',
|
|
ownership: [],
|
|
recommended: false,
|
|
modelCount: 8,
|
|
defaultModelId: null,
|
|
authMethods: ['api'],
|
|
actions: [
|
|
{
|
|
id: 'connect',
|
|
label: 'Connect',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: true,
|
|
ownershipScope: 'managed',
|
|
},
|
|
],
|
|
sources: ['opencode-provider'],
|
|
sourceLabel: 'OpenCode catalog',
|
|
providerSource: 'models.dev',
|
|
detail: 'App-managed API-key setup is available for this provider',
|
|
metadata: {
|
|
hasKnownModels: true,
|
|
requiresManualConfig: false,
|
|
supportedInlineAuth: true,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('115 OpenCode providers');
|
|
expect(host.textContent).toContain('DeepSeek');
|
|
expect(host.textContent).toContain('Cloudflare Workers AI');
|
|
expect(host.textContent).toContain('62 models');
|
|
expect(host.textContent).toContain('OpenCode catalog');
|
|
expect(host.querySelector('[data-testid="runtime-provider-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).not.toHaveBeenCalled();
|
|
expect(actions.startConnect).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
host
|
|
.querySelector('[data-testid="runtime-provider-directory-row-cloudflare-workers-ai"]')
|
|
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.startConnect).toHaveBeenCalledWith('cloudflare-workers-ai');
|
|
expect(actions.selectDirectoryProvider).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('shows an explicit zero-provider catalog count', 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({
|
|
directoryLoaded: true,
|
|
directoryTotalCount: 0,
|
|
directoryEntries: [],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('0 OpenCode providers');
|
|
expect(host.textContent).not.toContain('OpenCode provider catalog.');
|
|
});
|
|
|
|
it('uses singular provider catalog copy for one provider', 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({
|
|
directoryLoaded: true,
|
|
directoryTotalCount: 1,
|
|
directoryEntries: [],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('1 OpenCode provider.');
|
|
expect(host.textContent).not.toContain('1 OpenCode providers');
|
|
});
|
|
|
|
it('renders every advertised directory action instead of hiding configure behind connect', 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({
|
|
directoryLoaded: true,
|
|
directoryTotalCount: 1,
|
|
directoryEntries: [
|
|
{
|
|
providerId: 'manual-connectable',
|
|
displayName: 'Manual Connectable',
|
|
state: 'not-connected',
|
|
setupKind: 'connect-api-key',
|
|
ownership: [],
|
|
recommended: false,
|
|
modelCount: 1,
|
|
defaultModelId: null,
|
|
authMethods: ['api'],
|
|
actions: [
|
|
{
|
|
id: 'connect',
|
|
label: 'Connect',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: true,
|
|
ownershipScope: 'managed',
|
|
},
|
|
{
|
|
id: 'configure',
|
|
label: 'Configure manually',
|
|
enabled: false,
|
|
disabledReason: 'Manual fallback is also available',
|
|
requiresSecret: false,
|
|
ownershipScope: 'runtime',
|
|
},
|
|
],
|
|
sources: ['opencode-provider'],
|
|
sourceLabel: 'OpenCode catalog',
|
|
providerSource: 'models.dev',
|
|
detail: null,
|
|
metadata: {
|
|
hasKnownModels: true,
|
|
requiresManualConfig: true,
|
|
supportedInlineAuth: true,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const row = host.querySelector(
|
|
'[data-testid="runtime-provider-directory-row-manual-connectable"]'
|
|
);
|
|
const actionLabels = Array.from(row?.querySelectorAll('button') ?? []).map((button) =>
|
|
button.textContent?.trim()
|
|
);
|
|
|
|
expect(actionLabels).toContain('Connect');
|
|
expect(actionLabels).toContain('Configure manually');
|
|
});
|
|
|
|
it('uses the unified 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',
|
|
directoryLoaded: true,
|
|
directoryTotalCount: 1,
|
|
directoryEntries: [
|
|
{
|
|
providerId: 'deepseek',
|
|
displayName: 'DeepSeek',
|
|
state: 'available',
|
|
setupKind: 'available-readonly',
|
|
ownership: [],
|
|
recommended: false,
|
|
modelCount: 62,
|
|
defaultModelId: null,
|
|
authMethods: [],
|
|
actions: [],
|
|
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('DeepSeek');
|
|
expect(host.textContent).not.toContain('Search all OpenCode providers');
|
|
});
|
|
|
|
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-plus',
|
|
displayName: 'qwen/qwen3-coder-plus',
|
|
sourceLabel: 'OpenRouter',
|
|
free: false,
|
|
default: false,
|
|
availability: 'untested',
|
|
},
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/openai/gpt-oss-120b:free',
|
|
displayName: 'openai/gpt-oss-120b:free',
|
|
sourceLabel: 'OpenRouter',
|
|
free: true,
|
|
default: false,
|
|
availability: 'untested',
|
|
},
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'opencode/minimax-m2.5-free',
|
|
displayName: 'minimax-m2.5-free',
|
|
sourceLabel: 'OpenCode',
|
|
free: true,
|
|
default: false,
|
|
availability: 'untested',
|
|
},
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/mistralai/codestral-2508',
|
|
displayName: 'mistralai/codestral-2508',
|
|
sourceLabel: 'OpenRouter',
|
|
free: false,
|
|
default: false,
|
|
availability: 'untested',
|
|
},
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/anthropic/claude-sonnet-4.6',
|
|
displayName: 'anthropic/claude-sonnet-4.6',
|
|
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('Recommended');
|
|
expect(host.textContent).toContain('Not recommended');
|
|
expect(host.textContent).toContain('Not verified in OpenCode');
|
|
expect(host.textContent).toContain('Tested');
|
|
expect(host.textContent).toContain('Tested with limits');
|
|
expect(host.textContent).toContain('Recommended only');
|
|
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'
|
|
);
|
|
expect(connectedBadge).toBeInstanceOf(HTMLSpanElement);
|
|
expect(connectedBadge?.style.color).toBeTruthy();
|
|
const modelSearch = host.querySelector<HTMLInputElement>(
|
|
'[data-testid="runtime-provider-model-search"]'
|
|
);
|
|
const modelList = host.querySelector<HTMLElement>(
|
|
'[data-testid="runtime-provider-model-list"]'
|
|
);
|
|
expect(modelSearch?.style.paddingLeft).toBe('42px');
|
|
expect(modelList?.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<HTMLElement>(
|
|
'[data-testid="runtime-provider-model-result-openrouter/openai/gpt-oss-20b:free"]'
|
|
);
|
|
expect(modelResult).toBeInstanceOf(HTMLElement);
|
|
expect(modelResult?.style.color).toBe('#86efac');
|
|
expect((host.textContent ?? '').indexOf('mistralai/codestral-2508')).toBeLessThan(
|
|
(host.textContent ?? '').indexOf('qwen/qwen3-coder-plus')
|
|
);
|
|
expect((host.textContent ?? '').indexOf('opencode/big-pickle')).toBeLessThan(
|
|
(host.textContent ?? '').indexOf('minimax-m2.5-free')
|
|
);
|
|
expect((host.textContent ?? '').indexOf('mistralai/codestral-2508')).toBeLessThan(
|
|
(host.textContent ?? '').indexOf('minimax-m2.5-free')
|
|
);
|
|
expect((host.textContent ?? '').indexOf('minimax-m2.5-free')).toBeLessThan(
|
|
(host.textContent ?? '').indexOf('qwen/qwen3-coder-plus')
|
|
);
|
|
expect((host.textContent ?? '').indexOf('qwen/qwen3-coder-plus')).toBeLessThan(
|
|
(host.textContent ?? '').indexOf('openrouter/openai/gpt-oss-20b:free')
|
|
);
|
|
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');
|
|
expect(actions.selectProvider).not.toHaveBeenCalled();
|
|
|
|
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 KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.useModelForNewTeams).not.toHaveBeenCalled();
|
|
|
|
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('keeps the model search input enabled while model results are loading', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const connectedProvider = {
|
|
...createState().view!.providers[0],
|
|
state: 'connected' as const,
|
|
ownership: ['managed'] as const,
|
|
modelCount: 174,
|
|
actions: [
|
|
{
|
|
id: 'use' as const,
|
|
label: 'Use',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: false,
|
|
ownershipScope: 'runtime' as const,
|
|
},
|
|
],
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
providers: [connectedProvider],
|
|
},
|
|
providers: [connectedProvider],
|
|
selectedProviderId: 'openrouter',
|
|
modelPickerProviderId: 'openrouter',
|
|
modelPickerMode: 'use',
|
|
modelQuery: 'claude',
|
|
modelsLoading: true,
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const searchInput = host.querySelector<HTMLInputElement>(
|
|
'[data-testid="runtime-provider-model-search"]'
|
|
);
|
|
|
|
expect(searchInput).not.toBeNull();
|
|
expect(searchInput?.disabled).toBe(false);
|
|
expect(searchInput?.value).toBe('claude');
|
|
expect(host.querySelector('[data-testid="runtime-provider-model-loading-skeleton"]')).not.toBe(
|
|
null
|
|
);
|
|
});
|
|
|
|
it('does not expose disabled model rows as active buttons', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const connectedProvider = {
|
|
...createState().view!.providers[0],
|
|
state: 'connected' as const,
|
|
ownership: ['managed'] as const,
|
|
modelCount: 1,
|
|
actions: [],
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
providers: [connectedProvider],
|
|
},
|
|
providers: [connectedProvider],
|
|
selectedProviderId: 'openrouter',
|
|
modelPickerProviderId: 'openrouter',
|
|
modelPickerMode: 'use',
|
|
models: [
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/google/gemini-3-flash-preview',
|
|
displayName: 'google/gemini-3-flash-preview',
|
|
sourceLabel: 'OpenRouter',
|
|
free: false,
|
|
default: false,
|
|
availability: 'untested',
|
|
},
|
|
],
|
|
}),
|
|
actions,
|
|
disabled: true,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const row = host.querySelector<HTMLElement>(
|
|
'[data-testid="runtime-provider-model-row-openrouter/google/gemini-3-flash-preview"]'
|
|
);
|
|
|
|
expect(row?.getAttribute('role')).toBeNull();
|
|
expect(row?.getAttribute('aria-disabled')).toBe('true');
|
|
expect(row?.tabIndex).toBe(-1);
|
|
|
|
await act(async () => {
|
|
row?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.useModelForNewTeams).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps directory provider models visible when a model row is selected', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const provider = {
|
|
providerId: 'openrouter',
|
|
displayName: 'OpenRouter',
|
|
state: 'connected' as const,
|
|
ownership: ['managed'] as const,
|
|
recommended: true,
|
|
modelCount: 174,
|
|
defaultModelId: null,
|
|
authMethods: ['api'] as const,
|
|
actions: [],
|
|
sources: ['opencode-provider'] as const,
|
|
sourceLabel: 'OpenCode catalog',
|
|
providerSource: 'models.dev',
|
|
detail: 'Connected via app-managed OpenCode credential',
|
|
setupKind: 'connected' as const,
|
|
metadata: {
|
|
hasKnownModels: true,
|
|
requiresManualConfig: false,
|
|
supportedInlineAuth: true,
|
|
},
|
|
};
|
|
const state = createState({
|
|
providers: [],
|
|
directoryLoaded: true,
|
|
directoryEntries: [provider],
|
|
directoryTotalCount: 1,
|
|
selectedProviderId: 'openrouter',
|
|
modelPickerProviderId: 'openrouter',
|
|
modelPickerMode: 'use',
|
|
models: [
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/google/gemini-3-flash-preview',
|
|
displayName: 'google/gemini-3-flash-preview',
|
|
sourceLabel: 'OpenRouter',
|
|
free: false,
|
|
default: false,
|
|
availability: 'untested',
|
|
},
|
|
],
|
|
});
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state,
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
host
|
|
.querySelector(
|
|
'[data-testid="runtime-provider-model-row-openrouter/google/gemini-3-flash-preview"]'
|
|
)
|
|
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.useModelForNewTeams).toHaveBeenCalledWith(
|
|
'openrouter/google/gemini-3-flash-preview'
|
|
);
|
|
expect(actions.selectDirectoryProvider).not.toHaveBeenCalled();
|
|
expect(host.textContent).toContain('google/gemini-3-flash-preview');
|
|
expect(host.textContent).not.toContain('No models found.');
|
|
});
|
|
|
|
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?.className).toContain('runtime-provider-brand-icon');
|
|
expect(logo?.querySelector('svg,img')).not.toBeNull();
|
|
expect(logo?.getAttribute('style')).toContain('--runtime-provider-brand-fallback-background');
|
|
expect(logo?.getAttribute('style')).toContain('--runtime-provider-brand-fallback-border');
|
|
if (logo?.querySelector('svg')) {
|
|
expect(logo.getAttribute('style')).toContain('--runtime-provider-brand-fallback-color');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('uses Models.dev logos only for verified providers and initials for unknown 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: 'xai', displayName: 'xAI', logo: 'xai' },
|
|
{ providerId: 'groq', displayName: 'Groq', logo: 'groq' },
|
|
{ providerId: 'deepseek', displayName: 'DeepSeek', logo: 'deepseek' },
|
|
{ providerId: 'cohere', displayName: 'Cohere', logo: 'cohere' },
|
|
{
|
|
providerId: 'cloudferro-sherlock',
|
|
displayName: 'CloudFerro Sherlock',
|
|
logo: 'cloudferro-sherlock',
|
|
},
|
|
{ providerId: 'clarifai', displayName: 'Clarifai', label: 'CL' },
|
|
{ providerId: 'unknown-provider', displayName: 'Unknown Provider', label: 'UN' },
|
|
].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}"]`
|
|
);
|
|
if ('logo' in provider) {
|
|
const image = logo?.querySelector('img') as HTMLImageElement | null;
|
|
expect(image?.src).toContain(`https://models.dev/logos/${provider.logo}.svg`);
|
|
expect(logo?.className).toContain('runtime-provider-brand-icon');
|
|
} else {
|
|
expect(logo?.textContent).toBe(provider.label);
|
|
}
|
|
}
|
|
});
|
|
});
|