Files
2026-07-13 13:20:22 +08:00

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'
);
});
});