194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
import React, { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
useRuntimeProviderManagement,
|
|
type RuntimeProviderManagementActions,
|
|
type RuntimeProviderManagementState,
|
|
} from '../../../../src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement';
|
|
import {
|
|
getStoredCreateTeamModel,
|
|
getStoredCreateTeamProvider,
|
|
} from '../../../../src/renderer/services/createTeamPreferences';
|
|
|
|
import type { ElectronAPI } from '../../../../src/shared/types/api';
|
|
import type { RuntimeProviderManagementModelTestResponse } from '../../../../src/features/runtime-provider-management/contracts';
|
|
|
|
function installRuntimeProviderManagementApi(response: RuntimeProviderManagementModelTestResponse): void {
|
|
Object.defineProperty(window, 'electronAPI', {
|
|
configurable: true,
|
|
value: {
|
|
runtimeProviderManagement: {
|
|
testModel: vi.fn(() => Promise.resolve(response)),
|
|
},
|
|
} as unknown as ElectronAPI,
|
|
});
|
|
}
|
|
|
|
describe('useRuntimeProviderManagement', () => {
|
|
let host: HTMLDivElement;
|
|
let state: RuntimeProviderManagementState | null = null;
|
|
let actions: RuntimeProviderManagementActions | null = null;
|
|
|
|
function Harness(): React.ReactElement {
|
|
const hook = useRuntimeProviderManagement({
|
|
runtimeId: 'opencode',
|
|
enabled: false,
|
|
});
|
|
state = hook[0];
|
|
actions = hook[1];
|
|
return React.createElement('div');
|
|
}
|
|
|
|
function EnabledHarness(props: { projectPath?: string | null }): React.ReactElement {
|
|
const hook = useRuntimeProviderManagement({
|
|
runtimeId: 'opencode',
|
|
enabled: true,
|
|
projectPath: props.projectPath,
|
|
});
|
|
state = hook[0];
|
|
actions = hook[1];
|
|
return React.createElement('div');
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
window.localStorage.clear();
|
|
state = null;
|
|
actions = null;
|
|
});
|
|
|
|
afterEach(() => {
|
|
Reflect.deleteProperty(window, 'electronAPI');
|
|
document.body.innerHTML = '';
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('uses a clicked model as the app default for new teams without a global success banner', async () => {
|
|
const modelId = 'openrouter/openai/gpt-oss-20b:free';
|
|
const root = createRoot(host);
|
|
await act(async () => {
|
|
root.render(React.createElement(Harness));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
act(() => {
|
|
actions?.useModelForNewTeams(modelId);
|
|
});
|
|
|
|
expect(state?.selectedModelId).toBe(modelId);
|
|
expect(state?.successMessage).toBeNull();
|
|
expect(getStoredCreateTeamProvider()).toBe('opencode');
|
|
expect(getStoredCreateTeamModel('opencode')).toBe(modelId);
|
|
});
|
|
|
|
it('passes projectPath to the runtime provider management API', async () => {
|
|
const loadView = vi.fn(() =>
|
|
Promise.resolve({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.0.0',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
})
|
|
);
|
|
Object.defineProperty(window, 'electronAPI', {
|
|
configurable: true,
|
|
value: {
|
|
runtimeProviderManagement: {
|
|
loadView,
|
|
},
|
|
} as unknown as ElectronAPI,
|
|
});
|
|
|
|
const root = createRoot(host);
|
|
await act(async () => {
|
|
root.render(React.createElement(EnabledHarness, { projectPath: '/tmp/project-a' }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(loadView).toHaveBeenCalledWith({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/tmp/project-a',
|
|
});
|
|
});
|
|
|
|
it('keeps failed model probes scoped to the model result instead of a global success banner', async () => {
|
|
const modelId = 'openrouter/anthropic/claude-3.5-haiku';
|
|
const message =
|
|
'This request requires more credits, or fewer max_tokens. You requested up to 8192 tokens, but can only afford 381.';
|
|
installRuntimeProviderManagementApi({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
result: {
|
|
providerId: 'openrouter',
|
|
modelId,
|
|
ok: false,
|
|
availability: 'unavailable',
|
|
message,
|
|
diagnostics: [],
|
|
},
|
|
});
|
|
|
|
const root = createRoot(host);
|
|
await act(async () => {
|
|
root.render(React.createElement(Harness));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
await actions?.testModel('openrouter', modelId);
|
|
});
|
|
|
|
expect(state?.successMessage).toBeNull();
|
|
expect(state?.error).toBeNull();
|
|
expect(state?.modelResults[modelId]?.ok).toBe(false);
|
|
expect(state?.modelResults[modelId]?.message).toBe(message);
|
|
});
|
|
|
|
it('keeps successful model probes scoped to the model card instead of a global success banner', async () => {
|
|
const modelId = 'openrouter/openai/gpt-oss-20b:free';
|
|
installRuntimeProviderManagementApi({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
result: {
|
|
providerId: 'openrouter',
|
|
modelId,
|
|
ok: true,
|
|
availability: 'available',
|
|
message: 'Model probe passed',
|
|
diagnostics: [],
|
|
},
|
|
});
|
|
|
|
const root = createRoot(host);
|
|
await act(async () => {
|
|
root.render(React.createElement(Harness));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
await actions?.testModel('openrouter', modelId);
|
|
});
|
|
|
|
expect(state?.successMessage).toBeNull();
|
|
expect(state?.error).toBeNull();
|
|
expect(state?.modelResults[modelId]?.ok).toBe(true);
|
|
expect(state?.modelResults[modelId]?.message).toBe('Model probe passed');
|
|
});
|
|
});
|