219 lines
6.4 KiB
TypeScript
219 lines
6.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { IMAGE_GEN_ENV_KEYS } from '@/common/config/imageGenerationMcpEnv';
|
|
import { BUILTIN_IMAGE_GEN_NAME, type IMcpServer, type IProvider } from '@/common/config/storage';
|
|
import { resolveImageGenerationMigrationConfig, runBackendMigrations } from '@/process/utils/runBackendMigrations';
|
|
|
|
const {
|
|
batchImportServersMock,
|
|
configFileGetMock,
|
|
configFileSetMock,
|
|
httpRequestMock,
|
|
listServersMock,
|
|
testMcpConnectionMock,
|
|
updateServerMock,
|
|
} = vi.hoisted(() => ({
|
|
batchImportServersMock: vi.fn(),
|
|
configFileGetMock: vi.fn(),
|
|
configFileSetMock: vi.fn(),
|
|
httpRequestMock: vi.fn(),
|
|
listServersMock: vi.fn(),
|
|
testMcpConnectionMock: vi.fn(),
|
|
updateServerMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/common/adapter/httpBridge', () => ({
|
|
httpRequest: httpRequestMock,
|
|
}));
|
|
|
|
vi.mock('@/common/adapter/ipcBridge', () => ({
|
|
mcpService: {
|
|
listServers: { invoke: listServersMock },
|
|
batchImportServers: { invoke: batchImportServersMock },
|
|
updateServer: { invoke: updateServerMock },
|
|
testMcpConnection: { invoke: testMcpConnectionMock },
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/common/config/configMigration', () => ({
|
|
migrateConfigStorage: vi.fn().mockResolvedValue(undefined),
|
|
migrateLegacyMcpConfigToDb: vi.fn().mockResolvedValue(undefined),
|
|
migrateProviders: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
vi.mock('@/process/utils/initStorage', () => ({
|
|
getBuiltinMcpScriptPath: (name: string) => `/mock/${name}.js`,
|
|
}));
|
|
|
|
vi.mock('@/process/utils/migrateAssistants', () => ({
|
|
migrateAssistantsToBackend: vi.fn().mockResolvedValue(true),
|
|
}));
|
|
|
|
const provider: IProvider = {
|
|
id: 'provider-1',
|
|
platform: 'gemini',
|
|
name: 'Gemini',
|
|
base_url: 'https://generativelanguage.googleapis.com',
|
|
api_key: 'provider-key',
|
|
models: ['gemini-image'],
|
|
enabled: true,
|
|
};
|
|
|
|
const imageEnv = {
|
|
[IMAGE_GEN_ENV_KEYS.providerId]: 'provider-1',
|
|
[IMAGE_GEN_ENV_KEYS.platform]: 'gemini',
|
|
[IMAGE_GEN_ENV_KEYS.baseUrl]: 'https://generativelanguage.googleapis.com',
|
|
[IMAGE_GEN_ENV_KEYS.apiKey]: 'provider-key',
|
|
[IMAGE_GEN_ENV_KEYS.model]: 'gemini-image',
|
|
};
|
|
|
|
const imageServer = (): IMcpServer => ({
|
|
id: 'image-server-id',
|
|
name: BUILTIN_IMAGE_GEN_NAME,
|
|
description: 'Built-in image generation tool powered by AI models. Configure the model in Settings > Tools.',
|
|
enabled: true,
|
|
builtin: true,
|
|
transport: {
|
|
type: 'stdio',
|
|
command: 'node',
|
|
args: ['/mock/builtin-mcp-image-gen.js'],
|
|
env: imageEnv,
|
|
},
|
|
created_at: 1,
|
|
updated_at: 1,
|
|
original_json: JSON.stringify(
|
|
{
|
|
mcpServers: {
|
|
[BUILTIN_IMAGE_GEN_NAME]: {
|
|
command: 'node',
|
|
args: ['/mock/builtin-mcp-image-gen.js'],
|
|
env: imageEnv,
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
),
|
|
});
|
|
|
|
const configFile = {
|
|
get: configFileGetMock,
|
|
set: configFileSetMock,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
configFileGetMock.mockResolvedValue(undefined);
|
|
configFileSetMock.mockResolvedValue(undefined);
|
|
batchImportServersMock.mockResolvedValue([]);
|
|
updateServerMock.mockImplementation(async ({ id, data }) => ({
|
|
...imageServer(),
|
|
id,
|
|
...data,
|
|
}));
|
|
testMcpConnectionMock.mockResolvedValue({ success: false, error: 'Command not found: npx' });
|
|
httpRequestMock.mockImplementation(async (method: string, path: string) => {
|
|
if (method === 'GET' && path === '/api/settings/client') {
|
|
return {
|
|
'tools.imageGenerationModel': {
|
|
id: 'provider-1',
|
|
name: 'Gemini',
|
|
platform: 'gemini',
|
|
use_model: 'gemini-image',
|
|
},
|
|
};
|
|
}
|
|
if (method === 'GET' && path === '/api/providers') {
|
|
return [provider];
|
|
}
|
|
return undefined;
|
|
});
|
|
});
|
|
|
|
describe('resolveImageGenerationMigrationConfig', () => {
|
|
it('uses backend client preference when local config file no longer has the image model', () => {
|
|
const backendConfig = {
|
|
id: 'gemini',
|
|
name: 'Gemini',
|
|
platform: 'gemini',
|
|
base_url: 'https://example.test',
|
|
api_key: 'backend-key',
|
|
use_model: 'gemini-image',
|
|
};
|
|
|
|
expect(resolveImageGenerationMigrationConfig({ 'tools.imageGenerationModel': backendConfig }, undefined)).toEqual(
|
|
backendConfig
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('runBackendMigrations', () => {
|
|
it('does not write image generation business config back to local config storage', async () => {
|
|
listServersMock.mockResolvedValue([imageServer()]);
|
|
configFileGetMock.mockImplementation(async (key: string) => {
|
|
if (key === 'tools.imageGenerationModel') {
|
|
return {
|
|
id: 'provider-1',
|
|
name: 'Gemini',
|
|
platform: 'gemini',
|
|
use_model: 'gemini-image',
|
|
switch: true,
|
|
};
|
|
}
|
|
return undefined;
|
|
});
|
|
httpRequestMock.mockImplementation(async (method: string, path: string) => {
|
|
if (method === 'GET' && path === '/api/settings/client') {
|
|
return {};
|
|
}
|
|
if (method === 'GET' && path === '/api/providers') {
|
|
return [provider];
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
await runBackendMigrations(configFile as never);
|
|
|
|
expect(configFileSetMock).not.toHaveBeenCalledWith('tools.imageGenerationModel', expect.anything());
|
|
});
|
|
|
|
it('does not sync the built-in image MCP server when bootstrap makes no effective change', async () => {
|
|
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
listServersMock.mockResolvedValue([imageServer()]);
|
|
|
|
await runBackendMigrations(configFile as never);
|
|
|
|
expect(updateServerMock).not.toHaveBeenCalled();
|
|
expect(testMcpConnectionMock).not.toHaveBeenCalled();
|
|
expect(infoSpy).toHaveBeenCalledWith(
|
|
'[Migration] image MCP bootstrap decision, server id: %s, transport changed: %s, json changed: %s, will update: %s',
|
|
'image-server-id',
|
|
'no',
|
|
'no',
|
|
'no'
|
|
);
|
|
});
|
|
|
|
it('does not sync agents when only the stored image MCP JSON representation differs', async () => {
|
|
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
listServersMock.mockResolvedValue([
|
|
{
|
|
...imageServer(),
|
|
original_json: '{"legacy":true}',
|
|
},
|
|
]);
|
|
|
|
await runBackendMigrations(configFile as never);
|
|
|
|
expect(updateServerMock).toHaveBeenCalledOnce();
|
|
expect(testMcpConnectionMock).not.toHaveBeenCalled();
|
|
expect(infoSpy).toHaveBeenCalledWith(
|
|
'[Migration] image MCP bootstrap decision, server id: %s, transport changed: %s, json changed: %s, will update: %s',
|
|
'image-server-id',
|
|
'no',
|
|
'yes',
|
|
'yes'
|
|
);
|
|
});
|
|
});
|