273 lines
8.9 KiB
TypeScript
273 lines
8.9 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
handleProviderRoutes,
|
|
importCurrentUserModelConfig,
|
|
normalizeImportedUserModelConfig,
|
|
} from '@electron/api/routes/providers';
|
|
import type { ProviderAccount } from '@electron/shared/providers/types';
|
|
|
|
const providerServiceMock = vi.hoisted(() => ({
|
|
getAccount: vi.fn(),
|
|
getAccountApiKey: vi.fn(),
|
|
getDefaultAccountId: vi.fn(),
|
|
updateAccount: vi.fn(),
|
|
setDefaultAccount: vi.fn(),
|
|
}));
|
|
const proxyAwareFetchMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('@electron/services/providers/provider-service', () => ({
|
|
getProviderService: () => providerServiceMock,
|
|
}));
|
|
|
|
vi.mock('@electron/utils/proxy-fetch', () => ({
|
|
proxyAwareFetch: (...args: unknown[]) => proxyAwareFetchMock(...args),
|
|
}));
|
|
|
|
function createRequest(method: string, body?: unknown): IncomingMessage {
|
|
const request = new EventEmitter();
|
|
Object.assign(request, {
|
|
method,
|
|
headers: body === undefined ? {} : { 'content-type': 'application/json' },
|
|
[Symbol.asyncIterator]: async function* () {
|
|
if (body !== undefined) yield Buffer.from(JSON.stringify(body));
|
|
},
|
|
});
|
|
return request as IncomingMessage;
|
|
}
|
|
|
|
function createResponse() {
|
|
const chunks: string[] = [];
|
|
const response = {
|
|
statusCode: 0,
|
|
setHeader: vi.fn(),
|
|
end: vi.fn((chunk?: string) => {
|
|
if (chunk) chunks.push(chunk);
|
|
}),
|
|
} as unknown as ServerResponse;
|
|
return {
|
|
response,
|
|
get statusCode() { return response.statusCode; },
|
|
json: () => JSON.parse(chunks.join('')) as unknown,
|
|
};
|
|
}
|
|
|
|
function account(overrides: Partial<ProviderAccount> = {}): ProviderAccount {
|
|
return {
|
|
id: 'account-1',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
authMode: 'api_key',
|
|
enabled: true,
|
|
isDefault: true,
|
|
createdAt: '2026-08-24T00:00:00.000Z',
|
|
updatedAt: '2026-08-24T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('provider host api routes', () => {
|
|
const markProviderStale = vi.fn();
|
|
const context = {
|
|
codingProducts: { conversations: { markProviderStale } },
|
|
} as never;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
providerServiceMock.getAccount.mockResolvedValue(account());
|
|
providerServiceMock.getAccountApiKey.mockResolvedValue('stored-key');
|
|
providerServiceMock.updateAccount.mockResolvedValue(account({
|
|
updatedAt: '2026-08-24T01:00:00.000Z',
|
|
}));
|
|
providerServiceMock.getDefaultAccountId.mockResolvedValue('account-1');
|
|
providerServiceMock.setDefaultAccount.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('normalizes optional Works model capabilities before storing the account', () => {
|
|
expect(normalizeImportedUserModelConfig({
|
|
label: 'Makelore Models',
|
|
base_url: 'https://gateway.test/v1',
|
|
api_key: 'gateway-key',
|
|
credential_mode: 'works_square_ai_gateway',
|
|
models: ['deepseek/deepseek-v4-pro', 'qwen3.8-max'],
|
|
model_capabilities: {
|
|
'deepseek/deepseek-v4-pro': {
|
|
reasoning_efforts: ['max', 'low', 'medium', 'low'],
|
|
reasoning_can_disable: true,
|
|
},
|
|
'qwen3.8-max': {
|
|
reasoning_efforts: ['high'],
|
|
reasoning_can_disable: false,
|
|
},
|
|
'not-provisioned': {
|
|
reasoning_efforts: ['low'],
|
|
reasoning_can_disable: true,
|
|
},
|
|
},
|
|
})).toMatchObject({
|
|
models: ['deepseek-v4-pro', 'qwen3.8-max'],
|
|
modelCapabilities: {
|
|
'deepseek-v4-pro': {
|
|
reasoningEfforts: ['low', 'max'],
|
|
reasoningCanDisable: true,
|
|
},
|
|
'qwen3.8-max': {
|
|
reasoningEfforts: ['high'],
|
|
reasoningCanDisable: false,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('persists server capabilities and invalidates the runtime when they change', async () => {
|
|
const existing = account({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
model: 'deepseek-v4-pro',
|
|
metadata: {
|
|
customModels: ['deepseek-v4-pro'],
|
|
worksSquareModelCapabilities: {
|
|
'deepseek-v4-pro': {
|
|
reasoningEfforts: ['high'],
|
|
reasoningCanDisable: false,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
providerServiceMock.getAccount.mockResolvedValue(existing);
|
|
providerServiceMock.updateAccount.mockImplementation(async (_accountId: string, patch: Partial<ProviderAccount>) => ({
|
|
...existing,
|
|
...patch,
|
|
}));
|
|
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({
|
|
label: 'Makelore Models',
|
|
base_url: 'https://gateway.test/v1',
|
|
api_key: 'gateway-key',
|
|
credential_mode: 'api_key',
|
|
models: ['deepseek/deepseek-v4-pro'],
|
|
model_capabilities: {
|
|
'deepseek/deepseek-v4-pro': {
|
|
reasoning_efforts: ['low', 'high', 'max'],
|
|
reasoning_can_disable: true,
|
|
},
|
|
},
|
|
}), { status: 200, headers: { 'content-type': 'application/json' } }));
|
|
|
|
const imported = await importCurrentUserModelConfig(context, 'access-token');
|
|
|
|
expect(imported.account.metadata?.worksSquareModelCapabilities).toEqual({
|
|
'deepseek-v4-pro': {
|
|
reasoningEfforts: ['low', 'high', 'max'],
|
|
reasoningCanDisable: true,
|
|
},
|
|
});
|
|
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
|
|
'niancode-user-models',
|
|
expect.objectContaining({
|
|
metadata: expect.objectContaining({
|
|
worksSquareModelCapabilities: {
|
|
'deepseek-v4-pro': {
|
|
reasoningEfforts: ['low', 'high', 'max'],
|
|
reasoningCanDisable: true,
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
'gateway-key',
|
|
);
|
|
expect(markProviderStale).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('clears a previous server capability override when the optional field is absent', async () => {
|
|
const existing = account({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
model: 'deepseek-v4-pro',
|
|
metadata: {
|
|
customModels: ['deepseek-v4-pro'],
|
|
worksSquareModelCapabilities: {
|
|
'deepseek-v4-pro': {
|
|
reasoningEfforts: ['max'],
|
|
reasoningCanDisable: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
providerServiceMock.getAccount.mockResolvedValue(existing);
|
|
providerServiceMock.updateAccount.mockImplementation(async (_accountId: string, patch: Partial<ProviderAccount>) => ({
|
|
...existing,
|
|
...patch,
|
|
}));
|
|
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({
|
|
base_url: 'https://gateway.test/v1',
|
|
api_key: 'gateway-key',
|
|
models: ['deepseek/deepseek-v4-pro'],
|
|
}), { status: 200, headers: { 'content-type': 'application/json' } }));
|
|
|
|
const imported = await importCurrentUserModelConfig(context, 'access-token');
|
|
|
|
expect(imported.account.metadata).not.toHaveProperty('worksSquareModelCapabilities');
|
|
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
|
|
'niancode-user-models',
|
|
expect.objectContaining({
|
|
metadata: expect.not.objectContaining({ worksSquareModelCapabilities: expect.anything() }),
|
|
}),
|
|
'gateway-key',
|
|
);
|
|
});
|
|
|
|
it('marks Pi provider input stale after an account credential update', async () => {
|
|
const result = createResponse();
|
|
const handled = await handleProviderRoutes(
|
|
createRequest('PUT', { updates: {}, apiKey: 'sk-new' }),
|
|
result.response,
|
|
new URL('http://127.0.0.1/api/provider-accounts/account-1'),
|
|
context,
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(result.statusCode).toBe(200);
|
|
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith('account-1', {}, 'sk-new');
|
|
expect(markProviderStale).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not invalidate provider input for an unchanged account', async () => {
|
|
const result = createResponse();
|
|
await handleProviderRoutes(
|
|
createRequest('PUT', { updates: {} }),
|
|
result.response,
|
|
new URL('http://127.0.0.1/api/provider-accounts/account-1'),
|
|
context,
|
|
);
|
|
|
|
expect(result.statusCode).toBe(200);
|
|
expect(result.json()).toMatchObject({ success: true, noChange: true });
|
|
expect(providerServiceMock.updateAccount).not.toHaveBeenCalled();
|
|
expect(markProviderStale).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('invalidates provider input only when the default account changes', async () => {
|
|
const unchanged = createResponse();
|
|
await handleProviderRoutes(
|
|
createRequest('PUT', { accountId: 'account-1' }),
|
|
unchanged.response,
|
|
new URL('http://127.0.0.1/api/provider-accounts/default'),
|
|
context,
|
|
);
|
|
expect(markProviderStale).not.toHaveBeenCalled();
|
|
|
|
providerServiceMock.getDefaultAccountId.mockResolvedValue('account-2');
|
|
const changed = createResponse();
|
|
await handleProviderRoutes(
|
|
createRequest('PUT', { accountId: 'account-1' }),
|
|
changed.response,
|
|
new URL('http://127.0.0.1/api/provider-accounts/default'),
|
|
context,
|
|
);
|
|
|
|
expect(providerServiceMock.setDefaultAccount).toHaveBeenCalledWith('account-1');
|
|
expect(markProviderStale).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|