2316 lines
78 KiB
TypeScript
2316 lines
78 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,
|
|
directoryErrorDiagnostics: null,
|
|
directoryEntries: [],
|
|
directoryTotalCount: null,
|
|
directoryNextCursor: null,
|
|
directoryLoaded: false,
|
|
directorySelectedProviderId: null,
|
|
directorySupported: true,
|
|
activeFormProviderId: null,
|
|
setupForm: null,
|
|
setupFormLoading: false,
|
|
setupFormError: null,
|
|
setupFormErrorDiagnostics: null,
|
|
setupSubmitError: null,
|
|
setupSubmitErrorDiagnostics: null,
|
|
setupMetadata: {},
|
|
apiKeyValue: '',
|
|
modelPickerProviderId: null,
|
|
modelPickerMode: null,
|
|
modelQuery: '',
|
|
models: [],
|
|
modelsLoading: false,
|
|
modelsError: null,
|
|
modelsErrorDiagnostics: null,
|
|
selectedModelId: null,
|
|
testingModelIds: [],
|
|
savingDefaultModelId: null,
|
|
modelResults: {},
|
|
loading: false,
|
|
savingProviderId: null,
|
|
error: null,
|
|
errorDiagnostics: 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()),
|
|
};
|
|
}
|
|
|
|
function setInputValue(input: HTMLInputElement, value: string): void {
|
|
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
if (!setter) {
|
|
throw new Error('HTMLInputElement value setter not found');
|
|
}
|
|
|
|
setter.call(input, value);
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
|
|
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 model routes');
|
|
expect(
|
|
host.querySelector('[data-testid="runtime-provider-model-loading-skeleton"]')
|
|
).not.toBeNull();
|
|
expect(host.querySelectorAll('.skeleton-shimmer').length).toBeGreaterThanOrEqual(8);
|
|
expect(host.textContent).toContain('Checking...');
|
|
const refreshButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
|
button.textContent?.includes('Checking...')
|
|
);
|
|
expect(refreshButton?.disabled).toBe(true);
|
|
|
|
expect(host.textContent).not.toContain('No launchable OpenCode model routes were reported yet');
|
|
});
|
|
|
|
it('renders runtime command errors with a readable headline and multiline details', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const message = [
|
|
'OpenCode provider settings could not read the runtime response.',
|
|
'Expected a JSON object from the Agent Teams runtime provider command.',
|
|
'Resolved runtime binary: /opt/homebrew/bin/opencode',
|
|
'Command: /opt/homebrew/bin/opencode runtime providers view --runtime opencode --json --compact',
|
|
'stdout preview:',
|
|
'Commands:',
|
|
' opencode providers',
|
|
].join('\n');
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({ error: message }),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const alert = host.querySelector<HTMLElement>('[data-testid="runtime-provider-error"]');
|
|
const details = alert?.querySelector('pre');
|
|
|
|
expect(alert?.getAttribute('role')).toBe('alert');
|
|
expect(alert?.textContent).toContain(
|
|
'OpenCode provider settings could not read the runtime response.'
|
|
);
|
|
expect(details?.textContent).toContain('Resolved runtime binary: /opt/homebrew/bin/opencode');
|
|
expect(details?.textContent).toContain(' opencode providers');
|
|
expect(details?.className).toContain('whitespace-pre-wrap');
|
|
expect(details?.className).toContain('font-mono');
|
|
});
|
|
|
|
it('copies fallback error text when structured diagnostics are unavailable', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const writeText = vi.fn((_text: string) => Promise.resolve());
|
|
const clipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: { writeText },
|
|
});
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
error: 'Runtime provider crashed\nstderr preview:\nmissing bun',
|
|
errorDiagnostics: null,
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
Array.from(host.querySelectorAll('button'))
|
|
.find((button) => button.textContent?.includes('Copy diagnostics'))
|
|
?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(writeText).toHaveBeenCalledWith(
|
|
'OpenCode provider settings diagnostics\n\nMessage:\nRuntime provider crashed\nstderr preview:\nmissing bun'
|
|
);
|
|
if (clipboardDescriptor) {
|
|
Object.defineProperty(navigator, 'clipboard', clipboardDescriptor);
|
|
} else {
|
|
Reflect.deleteProperty(navigator, 'clipboard');
|
|
}
|
|
});
|
|
|
|
it('copies diagnostics with the selection fallback when clipboard API is unavailable', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const clipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
|
|
const execCommandDescriptor = Object.getOwnPropertyDescriptor(document, 'execCommand');
|
|
const execCommand = vi.fn(() => true);
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: undefined,
|
|
});
|
|
Object.defineProperty(document, 'execCommand', {
|
|
configurable: true,
|
|
value: execCommand,
|
|
});
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
error: 'Runtime provider crashed\nstderr preview:\nmissing bun',
|
|
errorDiagnostics: null,
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
Array.from(host.querySelectorAll('button'))
|
|
.find((button) => button.textContent?.includes('Copy diagnostics'))
|
|
?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(execCommand).toHaveBeenCalledWith('copy');
|
|
expect(host.textContent).toContain('Copied');
|
|
expect(document.querySelector('textarea')).toBeNull();
|
|
if (clipboardDescriptor) {
|
|
Object.defineProperty(navigator, 'clipboard', clipboardDescriptor);
|
|
} else {
|
|
Reflect.deleteProperty(navigator, 'clipboard');
|
|
}
|
|
if (execCommandDescriptor) {
|
|
Object.defineProperty(document, 'execCommand', execCommandDescriptor);
|
|
} else {
|
|
Reflect.deleteProperty(document, 'execCommand');
|
|
}
|
|
});
|
|
|
|
it('renders structured runtime diagnostics and copies the full redacted report', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const writeText = vi.fn((_text: string) => Promise.resolve());
|
|
const clipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: { writeText },
|
|
});
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
error: 'OpenCode provider settings could not read the runtime response.',
|
|
errorDiagnostics: {
|
|
errorCode: 'runtime-unhealthy',
|
|
summary: 'OpenCode provider settings could not read the runtime response.',
|
|
likelyCause:
|
|
'The app is launching the OpenCode CLI itself instead of the Agent Teams runtime.',
|
|
binaryPath: '/opt/homebrew/bin/opencode',
|
|
command:
|
|
'/opt/homebrew/bin/opencode runtime providers view --runtime opencode --json --compact',
|
|
projectPath: '/Users/test/project',
|
|
exitCode: 1,
|
|
stderrPreview: 'Command failed before JSON',
|
|
stdoutPreview: 'Commands:\n opencode providers',
|
|
hints: [
|
|
'Check CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH and CLAUDE_CLI_PATH.',
|
|
'Those environment variables must not point to opencode.',
|
|
],
|
|
},
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Likely cause');
|
|
expect(host.textContent).toContain('/opt/homebrew/bin/opencode');
|
|
expect(host.textContent).toContain('Command failed before JSON');
|
|
expect(
|
|
host.querySelector('[data-testid="runtime-provider-error-stderr-preview"]')?.textContent
|
|
).toContain('stderr preview');
|
|
expect(
|
|
host.querySelector('[data-testid="runtime-provider-error-stdout-preview"]')?.textContent
|
|
).toContain('opencode providers');
|
|
|
|
await act(async () => {
|
|
Array.from(host.querySelectorAll('button'))
|
|
.find((button) => button.textContent?.includes('Copy diagnostics'))
|
|
?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(writeText).toHaveBeenCalledTimes(1);
|
|
expect(writeText.mock.calls[0][0]).toContain('OpenCode provider settings diagnostics');
|
|
expect(writeText.mock.calls[0][0]).toContain('Error code: runtime-unhealthy');
|
|
expect(writeText.mock.calls[0][0]).toContain('Resolved runtime binary: /opt/homebrew/bin/opencode');
|
|
expect(writeText.mock.calls[0][0]).toContain('stderr preview:');
|
|
expect(writeText.mock.calls[0][0]).toContain('stdout preview:');
|
|
expect(host.textContent).toContain('Copied');
|
|
if (clipboardDescriptor) {
|
|
Object.defineProperty(navigator, 'clipboard', clipboardDescriptor);
|
|
} else {
|
|
Reflect.deleteProperty(navigator, 'clipboard');
|
|
}
|
|
});
|
|
|
|
it('does not activate a provider row when copying model diagnostics', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const writeText = vi.fn((_text: string) => Promise.resolve());
|
|
const clipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: { writeText },
|
|
});
|
|
const actions = createActions();
|
|
const base = createState();
|
|
const provider = {
|
|
...base.view!.providers[0],
|
|
state: 'connected' as const,
|
|
modelCount: 2,
|
|
actions: [
|
|
{
|
|
id: 'test' as const,
|
|
label: 'Test',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: false,
|
|
ownershipScope: 'runtime' as const,
|
|
},
|
|
],
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...base.view!,
|
|
providers: [provider],
|
|
},
|
|
providers: [provider],
|
|
selectedProviderId: provider.providerId,
|
|
modelPickerProviderId: provider.providerId,
|
|
modelPickerMode: 'use',
|
|
modelsError: 'Model list failed',
|
|
modelsErrorDiagnostics: {
|
|
summary: 'Model list failed',
|
|
likelyCause: 'The runtime returned a malformed models response.',
|
|
binaryPath: '/repo/cli-dev',
|
|
command: '/repo/cli-dev runtime providers models --runtime opencode',
|
|
projectPath: '/Users/test/project',
|
|
exitCode: 1,
|
|
stderrPreview: 'bad models payload',
|
|
stdoutPreview: null,
|
|
hints: ['Retry after refreshing the runtime.'],
|
|
},
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
Array.from(host.querySelectorAll('button'))
|
|
.find((button) => button.textContent?.includes('Copy diagnostics'))
|
|
?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(writeText).toHaveBeenCalledTimes(1);
|
|
expect(actions.selectProvider).not.toHaveBeenCalled();
|
|
expect(actions.startConnect).not.toHaveBeenCalled();
|
|
if (clipboardDescriptor) {
|
|
Object.defineProperty(navigator, 'clipboard', clipboardDescriptor);
|
|
} else {
|
|
Reflect.deleteProperty(navigator, 'clipboard');
|
|
}
|
|
});
|
|
|
|
it('renders structured diagnostics in provider form and model picker errors', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const provider = {
|
|
...createState().view!.providers[0],
|
|
state: 'connected' as const,
|
|
modelCount: 4,
|
|
actions: [
|
|
{
|
|
id: 'test' as const,
|
|
label: 'Test',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: false,
|
|
ownershipScope: 'runtime' as const,
|
|
},
|
|
],
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
providers: [provider],
|
|
selectedProviderId: provider.providerId,
|
|
activeFormProviderId: provider.providerId,
|
|
modelPickerProviderId: provider.providerId,
|
|
modelPickerMode: 'use',
|
|
setupSubmitError: 'Provider connect failed before JSON.',
|
|
setupSubmitErrorDiagnostics: {
|
|
summary: 'Provider connect failed before JSON.',
|
|
likelyCause: 'The runtime command printed CLI help instead of JSON.',
|
|
binaryPath: '/opt/homebrew/bin/opencode',
|
|
command: '/opt/homebrew/bin/opencode runtime providers connect',
|
|
projectPath: null,
|
|
exitCode: 1,
|
|
stderrPreview: 'unknown command',
|
|
stdoutPreview: 'Commands:\n opencode providers',
|
|
hints: ['Check the resolved runtime binary.'],
|
|
},
|
|
modelsError: 'Provider models failed before JSON.',
|
|
modelsErrorDiagnostics: {
|
|
summary: 'Provider models failed before JSON.',
|
|
likelyCause: 'The runtime command printed CLI help instead of JSON.',
|
|
binaryPath: '/opt/homebrew/bin/opencode',
|
|
command: '/opt/homebrew/bin/opencode runtime providers models',
|
|
projectPath: null,
|
|
exitCode: 1,
|
|
stderrPreview: 'unknown command',
|
|
stdoutPreview: 'Commands:\n opencode providers',
|
|
hints: ['Check the resolved runtime binary.'],
|
|
},
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(
|
|
host.querySelector('[data-testid="runtime-provider-setup-submit-error"]')?.textContent
|
|
).toContain('Provider connect failed before JSON.');
|
|
expect(
|
|
host.querySelector('[data-testid="runtime-provider-setup-submit-error"]')?.textContent
|
|
).toContain('/opt/homebrew/bin/opencode');
|
|
expect(host.querySelector('[data-testid="runtime-provider-models-error"]')?.textContent).toContain(
|
|
'Provider models failed before JSON.'
|
|
);
|
|
expect(host.querySelector('[data-testid="runtime-provider-models-error"]')?.textContent).toContain(
|
|
'opencode providers'
|
|
);
|
|
});
|
|
|
|
it('renders provider directory errors with preserved multiline details', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const message = [
|
|
'OpenCode provider settings could not read the runtime response.',
|
|
'stderr preview:',
|
|
'runtime crashed before JSON',
|
|
].join('\n');
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
directoryError: message,
|
|
directoryLoaded: true,
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const alert = host.querySelector<HTMLElement>(
|
|
'[data-testid="runtime-provider-directory-error"]'
|
|
);
|
|
const details = alert?.querySelector('pre');
|
|
|
|
expect(alert?.getAttribute('role')).toBe('alert');
|
|
expect(details?.textContent).toContain('stderr preview:');
|
|
expect(details?.textContent).toContain('runtime crashed before JSON');
|
|
expect(details?.className).toContain('whitespace-pre-wrap');
|
|
});
|
|
|
|
it('keeps project context out of the runtime summary and labels it as validation context', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
configuredModels: [
|
|
{
|
|
providerId: 'llama.cpp',
|
|
modelId: 'llama.cpp/qwen-test:0.5b',
|
|
displayName: 'qwen-test:0.5b',
|
|
sourceLabel: 'llama.cpp',
|
|
free: false,
|
|
default: false,
|
|
availability: 'available',
|
|
accessKind: 'verified',
|
|
routeKind: 'configured_local',
|
|
proofState: 'verified',
|
|
requiresExecutionProof: false,
|
|
accessReason: null,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
projectPath: '/Users/belief/dev/projects/321',
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('OpenCode defaults');
|
|
expect(host.textContent).toContain('Validation context');
|
|
expect(host.textContent).toContain('Tests use 321. Default applies unless');
|
|
expect(host.textContent).not.toContain('Project context: 321');
|
|
expect(host.textContent).not.toContain('Current context: 321');
|
|
expect(host.textContent).not.toContain('Managing selected project profile');
|
|
expect(host.textContent).not.toContain('/Users/belief/dev/projects/321');
|
|
});
|
|
|
|
it('renders configured OpenCode model routes with local proof actions', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const configuredModel = {
|
|
providerId: 'llama.cpp',
|
|
modelId: 'llama.cpp/qwen-test:0.5b',
|
|
displayName: 'qwen-test:0.5b',
|
|
sourceLabel: 'llama.cpp',
|
|
free: false,
|
|
default: false,
|
|
availability: 'untested' as const,
|
|
accessKind: 'configured_authless' as const,
|
|
routeKind: 'configured_local' as const,
|
|
proofState: 'needs_probe' as const,
|
|
requiresExecutionProof: true,
|
|
accessReason: 'Execution proof required',
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
configuredModels: [configuredModel],
|
|
},
|
|
selectedModelId: 'llama.cpp/qwen-test:0.5b',
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
projectPath: '/tmp/project',
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const row = host.querySelector<HTMLElement>(
|
|
'[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]'
|
|
);
|
|
expect(host.textContent).toContain('OpenCode model routes');
|
|
expect(host.textContent).toContain('Known routes from OpenCode config');
|
|
expect(row?.textContent).toContain('local');
|
|
expect(row?.textContent).toContain('known route');
|
|
expect(row?.textContent).toContain('needs test');
|
|
|
|
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
|
await act(async () => {
|
|
buttons.find((button) => button.textContent?.includes('Test'))?.click();
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
buttons.find((button) => button.textContent?.includes('Save for team picker'))?.click();
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
buttons.find((button) => button.textContent?.includes('Set all-projects default'))?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.testModel).toHaveBeenCalledWith('llama.cpp', 'llama.cpp/qwen-test:0.5b');
|
|
expect(actions.useModelForNewTeams).toHaveBeenCalledWith('llama.cpp/qwen-test:0.5b');
|
|
expect(actions.setDefaultModel).toHaveBeenCalledWith(
|
|
'llama.cpp',
|
|
'llama.cpp/qwen-test:0.5b',
|
|
'all_projects'
|
|
);
|
|
});
|
|
|
|
it('can set an all-projects OpenCode default from the model scope controls', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const configuredModel = {
|
|
providerId: 'llama.cpp',
|
|
modelId: 'llama.cpp/qwen-test:0.5b',
|
|
displayName: 'qwen-test:0.5b',
|
|
sourceLabel: 'llama.cpp',
|
|
free: false,
|
|
default: false,
|
|
availability: 'available' as const,
|
|
accessKind: 'verified' as const,
|
|
routeKind: 'configured_local' as const,
|
|
proofState: 'verified' as const,
|
|
requiresExecutionProof: false,
|
|
accessReason: null,
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
configuredModels: [configuredModel],
|
|
},
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
projectPath: '/tmp/project-a',
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
Array.from(host.querySelectorAll('button'))
|
|
.find((button) => button.textContent?.includes('Set all-projects default'))
|
|
?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain(
|
|
'Default for every project that does not have its own OpenCode override'
|
|
);
|
|
expect(host.textContent).toContain('Validation context');
|
|
expect(actions.setDefaultModel).toHaveBeenCalledWith(
|
|
'llama.cpp',
|
|
'llama.cpp/qwen-test:0.5b',
|
|
'all_projects'
|
|
);
|
|
});
|
|
|
|
it('filters launchable OpenCode model routes by route text', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const localModel = {
|
|
providerId: 'llama.cpp',
|
|
modelId: 'llama.cpp/qwen-test:0.5b',
|
|
displayName: 'qwen-test:0.5b',
|
|
sourceLabel: 'llama.cpp',
|
|
free: false,
|
|
default: false,
|
|
availability: 'available' as const,
|
|
accessKind: 'verified' as const,
|
|
routeKind: 'configured_local' as const,
|
|
proofState: 'verified' as const,
|
|
requiresExecutionProof: false,
|
|
accessReason: null,
|
|
};
|
|
const freeModel = {
|
|
providerId: 'opencode',
|
|
modelId: 'opencode/big-pickle',
|
|
displayName: 'big-pickle',
|
|
sourceLabel: 'OpenCode',
|
|
free: true,
|
|
default: false,
|
|
availability: 'available' as const,
|
|
accessKind: 'builtin_free' as const,
|
|
routeKind: 'builtin_free' as const,
|
|
proofState: 'not_required' as const,
|
|
requiresExecutionProof: false,
|
|
accessReason: null,
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
configuredModels: [localModel, freeModel],
|
|
},
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
projectPath: '/tmp/project-a',
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const searchInput = host.querySelector<HTMLInputElement>(
|
|
'input[placeholder="Search model routes"]'
|
|
);
|
|
expect(searchInput).not.toBeNull();
|
|
expect(host.textContent).toContain('qwen-test:0.5b');
|
|
expect(host.textContent).toContain('big-pickle');
|
|
|
|
await act(async () => {
|
|
setInputValue(searchInput!, 'pickle');
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).not.toContain('qwen-test:0.5b');
|
|
expect(host.textContent).toContain('big-pickle');
|
|
|
|
await act(async () => {
|
|
setInputValue(searchInput!, 'missing-route');
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('No OpenCode model routes match');
|
|
expect(host.textContent).toContain('missing-route');
|
|
});
|
|
|
|
it('opens launchable routes first when they exist and keeps providers in a separate tab', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const baseState = createState();
|
|
const configuredModel = {
|
|
providerId: 'llama.cpp',
|
|
modelId: 'llama.cpp/qwen-test:0.5b',
|
|
displayName: 'qwen-test:0.5b',
|
|
sourceLabel: 'llama.cpp',
|
|
free: false,
|
|
default: false,
|
|
availability: 'untested' as const,
|
|
accessKind: 'configured_authless' as const,
|
|
routeKind: 'configured_local' as const,
|
|
proofState: 'needs_probe' as const,
|
|
requiresExecutionProof: true,
|
|
accessReason: 'Execution proof required',
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...baseState.view!,
|
|
configuredModels: [configuredModel],
|
|
},
|
|
providers: baseState.view?.providers ?? [],
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('OpenCode model routes');
|
|
expect(host.textContent).toContain('llama.cpp/qwen-test:0.5b');
|
|
expect(host.textContent).toContain(
|
|
'Select a validation context above to enable Test and Set default'
|
|
);
|
|
expect(host.textContent).toContain('Providers');
|
|
expect(host.querySelector('[data-testid="runtime-provider-row-openrouter"]')).toBeNull();
|
|
|
|
const row = host.querySelector<HTMLElement>(
|
|
'[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]'
|
|
);
|
|
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
|
expect(buttons.map((button) => [button.textContent?.trim(), button.disabled])).toEqual([
|
|
['Test', true],
|
|
['Save for team picker', false],
|
|
['Set all-projects default', true],
|
|
]);
|
|
expect(
|
|
Array.from(row?.querySelectorAll('[title]') ?? []).some(
|
|
(element) =>
|
|
element.getAttribute('title') ===
|
|
'Select a project context before testing or saving OpenCode defaults.'
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it('shows unknown OpenCode defaults without enabling launch actions', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const unknownDefaultModel = {
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/moonshotai/kimi-k2',
|
|
displayName: 'moonshotai/kimi-k2',
|
|
sourceLabel: 'OpenRouter',
|
|
free: false,
|
|
default: true,
|
|
availability: 'untested' as const,
|
|
accessKind: 'unknown_model' as const,
|
|
routeKind: 'catalog_provider' as const,
|
|
proofState: 'not_required' as const,
|
|
requiresExecutionProof: false,
|
|
accessReason: 'Model was not found in the live catalog',
|
|
};
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...createState().view!,
|
|
configuredModels: [unknownDefaultModel],
|
|
},
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const row = host.querySelector<HTMLElement>(
|
|
'[data-testid="configured-opencode-model-row-openrouter/moonshotai/kimi-k2"]'
|
|
);
|
|
expect(row?.textContent).toContain('unknown');
|
|
expect(row?.textContent).toContain('default');
|
|
|
|
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
|
expect(buttons.map((button) => button.disabled)).toEqual([true, true, true]);
|
|
expect(
|
|
Array.from(row?.querySelectorAll('[title]') ?? []).some(
|
|
(element) =>
|
|
element.getAttribute('title') ===
|
|
'This model is the current OpenCode default, but it is not available in the live catalog yet.'
|
|
)
|
|
).toBe(true);
|
|
await act(async () => {
|
|
buttons.forEach((button) => button.click());
|
|
await Promise.resolve();
|
|
});
|
|
expect(actions.testModel).not.toHaveBeenCalled();
|
|
expect(actions.useModelForNewTeams).not.toHaveBeenCalled();
|
|
expect(actions.setDefaultModel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('renders duplicate runtime diagnostics without React key warnings', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const actions = createActions();
|
|
const baseState = createState();
|
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
view: {
|
|
...baseState.view!,
|
|
diagnostics: [
|
|
'Unable to connect. Is the computer able to access the url?',
|
|
'Unable to connect. Is the computer able to access the url?',
|
|
],
|
|
},
|
|
providers: baseState.view?.providers ?? [],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const duplicateDiagnostics = host.textContent?.match(
|
|
/Unable to connect\. Is the computer able to access the url\?/g
|
|
);
|
|
const duplicateKeyWarnings = consoleError.mock.calls.filter((call) =>
|
|
call.some(
|
|
(argument) =>
|
|
typeof argument === 'string' &&
|
|
argument.includes('Encountered two children with the same key')
|
|
)
|
|
);
|
|
consoleError.mockRestore();
|
|
|
|
expect(duplicateDiagnostics).toHaveLength(2);
|
|
expect(duplicateKeyWarnings).toHaveLength(0);
|
|
});
|
|
|
|
it('renders duplicate structured diagnostic hints without React key warnings', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(RuntimeProviderManagementPanelView, {
|
|
state: createState({
|
|
error: 'OpenCode provider settings are using the wrong runtime binary.',
|
|
errorDiagnostics: {
|
|
summary: 'OpenCode provider settings are using the wrong runtime binary.',
|
|
likelyCause:
|
|
'The app resolved the OpenCode CLI itself as the Agent Teams runtime binary.',
|
|
binaryPath: '/opt/homebrew/bin/opencode',
|
|
command:
|
|
'/opt/homebrew/bin/opencode runtime providers view --runtime opencode --json --compact',
|
|
projectPath: null,
|
|
exitCode: null,
|
|
stderrPreview: null,
|
|
stdoutPreview: null,
|
|
hints: [
|
|
'Those environment variables must not point to opencode.',
|
|
'Those environment variables must not point to opencode.',
|
|
],
|
|
},
|
|
}),
|
|
actions: createActions(),
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const duplicateHints = host.textContent?.match(
|
|
/Those environment variables must not point to opencode\./g
|
|
);
|
|
const duplicateKeyWarnings = consoleError.mock.calls.filter((call) =>
|
|
call.some(
|
|
(argument) =>
|
|
typeof argument === 'string' &&
|
|
argument.includes('Encountered two children with the same key')
|
|
)
|
|
);
|
|
consoleError.mockRestore();
|
|
|
|
expect(duplicateHints).toHaveLength(2);
|
|
expect(duplicateKeyWarnings).toHaveLength(0);
|
|
});
|
|
|
|
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,
|
|
configuredAuthless: 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,
|
|
configuredAuthless: false,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
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,
|
|
configuredAuthless: false,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
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('opens model list for configured authless local directory providers', 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: 'llama.cpp',
|
|
displayName: 'llama.cpp',
|
|
state: 'available',
|
|
setupKind: 'available-readonly',
|
|
ownership: [],
|
|
recommended: false,
|
|
modelCount: 1,
|
|
defaultModelId: null,
|
|
authMethods: [],
|
|
actions: [
|
|
{
|
|
id: 'test',
|
|
label: 'Test',
|
|
enabled: true,
|
|
disabledReason: null,
|
|
requiresSecret: false,
|
|
ownershipScope: 'runtime',
|
|
},
|
|
],
|
|
sources: ['config-provider'],
|
|
sourceLabel: 'configured',
|
|
providerSource: null,
|
|
detail: 'Configured local OpenCode model route is available',
|
|
metadata: {
|
|
hasKnownModels: true,
|
|
requiresManualConfig: false,
|
|
supportedInlineAuth: false,
|
|
configuredAuthless: true,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const row = host.querySelector<HTMLElement>(
|
|
'[data-testid="runtime-provider-directory-row-llama.cpp"]'
|
|
);
|
|
expect(row?.textContent).toContain('Configured local');
|
|
|
|
await act(async () => {
|
|
row?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(actions.selectDirectoryProvider).toHaveBeenCalledWith('llama.cpp');
|
|
});
|
|
|
|
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,
|
|
configuredAuthless: 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,
|
|
projectPath: '/tmp/project',
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('openrouter/openai/gpt-oss-20b:free');
|
|
expect(host.textContent).toContain('Saved for team picker');
|
|
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() === 'Save for team picker'
|
|
)
|
|
).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('filters provider model picker rows to free models', 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: 2,
|
|
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/anthropic/claude-haiku-4.5',
|
|
displayName: 'anthropic/claude-haiku-4.5',
|
|
sourceLabel: 'OpenRouter',
|
|
free: true,
|
|
default: false,
|
|
availability: 'untested',
|
|
routeKind: 'connected_provider',
|
|
},
|
|
{
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/anthropic/claude-sonnet-4.6',
|
|
displayName: 'anthropic/claude-sonnet-4.6',
|
|
sourceLabel: 'OpenRouter',
|
|
free: false,
|
|
default: false,
|
|
availability: 'untested',
|
|
routeKind: 'connected_provider',
|
|
},
|
|
],
|
|
}),
|
|
actions,
|
|
disabled: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Free only');
|
|
expect(host.textContent).toContain('anthropic/claude-haiku-4.5');
|
|
expect(host.textContent).toContain('anthropic/claude-sonnet-4.6');
|
|
|
|
await act(async () => {
|
|
host.querySelector<HTMLElement>('#runtime-provider-openrouter-free-only')?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('anthropic/claude-haiku-4.5');
|
|
expect(host.textContent).not.toContain('anthropic/claude-sonnet-4.6');
|
|
});
|
|
|
|
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,
|
|
configuredAuthless: false,
|
|
},
|
|
};
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
});
|