mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 08:53:09 +00:00
263 lines
12 KiB
TypeScript
263 lines
12 KiB
TypeScript
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
|
|
|
const alphaModelRef = 'custom-alpha123/model-alpha';
|
|
const betaModelRef = 'custom-beta5678/provider/model-beta';
|
|
|
|
test.describe('ClawX chat model picker', () => {
|
|
test('switches the current agent model without requesting a gateway refresh', async ({ launchElectronApp }) => {
|
|
const app = await launchElectronApp({ skipSetup: true });
|
|
|
|
try {
|
|
await app.evaluate(async ({ app: _app }, refs) => {
|
|
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
|
|
|
|
let currentModelRef = refs.alphaModelRef;
|
|
const hostRequests: Array<{ path: string; method: string; body: unknown }> = [];
|
|
const now = new Date().toISOString();
|
|
let releaseProviderAccounts: (() => void) | undefined;
|
|
const providerAccountsReady = new Promise<void>((resolve) => {
|
|
releaseProviderAccounts = resolve;
|
|
});
|
|
const originalHostInvoke = (ipcMain as unknown as {
|
|
_invokeHandlers?: Map<string, (event: unknown, request: unknown) => Promise<unknown>>;
|
|
})._invokeHandlers?.get('host:invoke');
|
|
const makeResponse = (id: unknown, data: unknown) => ({
|
|
id: typeof id === 'string' ? id : undefined,
|
|
ok: true,
|
|
data,
|
|
});
|
|
|
|
const workspacePath = '/tmp/clawx-model-picker-workspace';
|
|
const agentsSnapshot = () => ({
|
|
success: true,
|
|
agents: [{
|
|
id: 'main',
|
|
name: 'Main',
|
|
isDefault: true,
|
|
modelDisplay: currentModelRef.split('/').slice(1).join('/'),
|
|
modelRef: currentModelRef,
|
|
overrideModelRef: currentModelRef,
|
|
inheritedModel: false,
|
|
workspace: workspacePath,
|
|
agentDir: '~/.openclaw/agents/main/agent',
|
|
mainSessionKey: 'agent:main:main',
|
|
channelTypes: [],
|
|
}],
|
|
defaultAgentId: 'main',
|
|
defaultModelRef: refs.alphaModelRef,
|
|
configuredChannelTypes: [],
|
|
channelOwners: {},
|
|
channelAccountOwners: {},
|
|
});
|
|
|
|
ipcMain.removeHandler('gateway:status');
|
|
ipcMain.handle('gateway:status', async () => ({ state: 'running', port: 18789, pid: 12345 }));
|
|
|
|
ipcMain.removeHandler('gateway:rpc');
|
|
ipcMain.handle('gateway:rpc', async (_event: unknown, method: string, params: unknown) => {
|
|
hostRequests.push({ path: `gateway:${method}`, method: 'RPC', body: params ?? null });
|
|
if (method === 'sessions.list') {
|
|
return { success: true, result: { sessions: [{ key: 'agent:main:main', displayName: 'main' }] } };
|
|
}
|
|
return { success: true, result: {} };
|
|
});
|
|
|
|
ipcMain.removeHandler('host:invoke');
|
|
ipcMain.handle('host:invoke', async (event: unknown, request: {
|
|
id?: string;
|
|
module?: string;
|
|
action?: string;
|
|
payload?: Record<string, unknown>;
|
|
}) => {
|
|
const body = request?.payload ?? null;
|
|
hostRequests.push({
|
|
path: `${request?.module ?? ''}:${request?.action ?? ''}`,
|
|
method: 'HOST',
|
|
body,
|
|
});
|
|
|
|
if (request?.module === 'gateway' && request.action === 'status') {
|
|
return makeResponse(request.id, { state: 'running', port: 18789, pid: 12345, gatewayReady: true });
|
|
}
|
|
if (request?.module === 'settings' && request.action === 'getAll') {
|
|
return makeResponse(request.id, {
|
|
language: 'en',
|
|
setupComplete: true,
|
|
chatWorkspacePath: workspacePath,
|
|
recentWorkspacePaths: [workspacePath],
|
|
});
|
|
}
|
|
if (request?.module === 'files' && request.action === 'resolveWorkspaceContext') {
|
|
const workspaceRoot = typeof body?.workspaceRoot === 'string' ? body.workspaceRoot.trim() : '';
|
|
const executionCwd = typeof body?.executionCwd === 'string' ? body.executionCwd.trim() : '';
|
|
if (!workspaceRoot || !executionCwd) {
|
|
return makeResponse(request.id, { ok: false, error: 'outsideSandbox' });
|
|
}
|
|
return makeResponse(request.id, { ok: true, workspaceRoot, executionCwd });
|
|
}
|
|
if (request?.module === 'chat' && request.action === 'loadAcpSession') {
|
|
return makeResponse(request.id, { success: true, generation: 1 });
|
|
}
|
|
if (request?.module === 'gateway' && request.action === 'rpc') {
|
|
const method = typeof body?.method === 'string' ? body.method : '';
|
|
const params = body?.params ?? null;
|
|
hostRequests.push({ path: `gateway:${method}`, method: 'RPC', body: params });
|
|
if (method === 'sessions.list') {
|
|
return makeResponse(request.id, { success: true, result: { sessions: [{ key: 'agent:main:main', displayName: 'main' }] } });
|
|
}
|
|
return makeResponse(request.id, { success: true, result: {} });
|
|
}
|
|
if (request?.module === 'agents' && request.action === 'list') {
|
|
return makeResponse(request.id, agentsSnapshot());
|
|
}
|
|
if (request?.module === 'agents' && request.action === 'updateModel') {
|
|
currentModelRef = typeof body?.modelRef === 'string' ? body.modelRef : refs.alphaModelRef;
|
|
hostRequests.push({
|
|
path: '/api/agents/main/model',
|
|
method: 'PUT',
|
|
body: { modelRef: currentModelRef },
|
|
});
|
|
return makeResponse(request.id, agentsSnapshot());
|
|
}
|
|
if (request?.module === 'providers' && request.action === 'accounts') {
|
|
await providerAccountsReady;
|
|
return makeResponse(request.id, [
|
|
{
|
|
id: 'alpha1234',
|
|
vendorId: 'custom',
|
|
label: 'Alpha',
|
|
authMode: 'api_key',
|
|
baseUrl: 'http://127.0.0.1:1111/v1',
|
|
model: 'model-alpha',
|
|
enabled: true,
|
|
isDefault: true,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
{
|
|
id: 'beta5678',
|
|
vendorId: 'custom',
|
|
label: 'Beta',
|
|
authMode: 'api_key',
|
|
baseUrl: 'http://127.0.0.1:2222/v1',
|
|
model: refs.betaModelRef,
|
|
enabled: true,
|
|
isDefault: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
{
|
|
id: 'openai-oauth',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
authMode: 'oauth_browser',
|
|
model: 'openai/gpt-5.6',
|
|
metadata: { customModels: ['gpt-5.5', 'openai/gpt-5.6'] },
|
|
enabled: true,
|
|
isDefault: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
{
|
|
id: 'moonshot-api-key',
|
|
vendorId: 'moonshot',
|
|
label: 'Moonshot',
|
|
authMode: 'api_key',
|
|
model: 'moonshot/kimi-k2.7',
|
|
metadata: { customModels: ['kimi-k2.6', 'moonshot/kimi-k2.7'] },
|
|
enabled: true,
|
|
isDefault: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
]);
|
|
}
|
|
if (request?.module === 'providers' && request.action === 'list') {
|
|
return makeResponse(request.id, [
|
|
{ id: 'alpha1234', type: 'custom', name: 'Alpha', enabled: true, hasKey: true, keyMasked: 'sk-***', createdAt: now, updatedAt: now },
|
|
{ id: 'beta5678', type: 'custom', name: 'Beta', enabled: true, hasKey: true, keyMasked: 'sk-***', createdAt: now, updatedAt: now },
|
|
]);
|
|
}
|
|
if (request?.module === 'providers' && request.action === 'accountKeyInfo') {
|
|
return makeResponse(request.id, [
|
|
{ accountId: 'alpha1234', hasKey: true, keyMasked: 'sk-***' },
|
|
{ accountId: 'beta5678', hasKey: true, keyMasked: 'sk-***' },
|
|
{ accountId: 'moonshot-api-key', hasKey: true, keyMasked: 'sk-***' },
|
|
]);
|
|
}
|
|
if (request?.module === 'providers' && request.action === 'vendors') {
|
|
return makeResponse(request.id, [
|
|
{ id: 'openai', name: 'OpenAI', supportedAuthModes: ['api_key', 'oauth_browser'] },
|
|
{ id: 'moonshot', name: 'Moonshot', supportedAuthModes: ['api_key'] },
|
|
]);
|
|
}
|
|
if (request?.module === 'providers' && request.action === 'getDefaultAccount') {
|
|
return makeResponse(request.id, { accountId: 'alpha1234' });
|
|
}
|
|
|
|
return originalHostInvoke?.(event, request) ?? makeResponse(request?.id, {});
|
|
});
|
|
|
|
(globalThis as typeof globalThis & { __chatModelPickerRequests?: typeof hostRequests }).__chatModelPickerRequests = hostRequests;
|
|
(globalThis as typeof globalThis & {
|
|
__releaseChatModelProviders?: () => void;
|
|
}).__releaseChatModelProviders = releaseProviderAccounts;
|
|
}, { alphaModelRef, betaModelRef });
|
|
|
|
const page = await getStableWindow(app);
|
|
await page.reload();
|
|
await expect(page.getByTestId('main-layout')).toBeVisible();
|
|
await expect.poll(async () => app.evaluate(() => (
|
|
(globalThis as typeof globalThis & {
|
|
__chatModelPickerRequests?: Array<{ path: string }>;
|
|
}).__chatModelPickerRequests?.some((request) => request.path === 'providers:accounts') ?? false
|
|
))).toBe(true);
|
|
expect(await app.evaluate(() => (
|
|
(globalThis as typeof globalThis & {
|
|
__chatModelPickerRequests?: Array<{ path: string }>;
|
|
}).__chatModelPickerRequests?.some((request) => request.path === 'agents:updateModel') ?? false
|
|
))).toBe(false);
|
|
await app.evaluate(() => {
|
|
(globalThis as typeof globalThis & {
|
|
__releaseChatModelProviders?: () => void;
|
|
}).__releaseChatModelProviders?.();
|
|
});
|
|
await app.evaluate(({ BrowserWindow }) => {
|
|
const win = BrowserWindow.getAllWindows()[0];
|
|
win?.webContents.send('gateway:status-changed', { state: 'running', port: 18789, pid: 12345, gatewayReady: true });
|
|
});
|
|
|
|
await expect(page.getByTestId('chat-model-picker-button')).toContainText('model-alpha (Alpha)');
|
|
await page.getByTestId('chat-model-picker-button').click();
|
|
await expect(page.getByTestId('chat-model-picker-menu')).toBeVisible();
|
|
await expect(page.getByTestId('chat-model-picker-menu')).toContainText('provider/model-beta (Beta)');
|
|
await expect(page.getByTestId('chat-model-picker-menu')).toContainText('gpt-5.6 (OpenAI)');
|
|
await expect(page.getByTestId('chat-model-picker-menu')).not.toContainText('gpt-5.5 (OpenAI)');
|
|
await expect(page.getByTestId('chat-model-picker-menu')).not.toContainText('openai/gpt-5.6 (OpenAI)');
|
|
await expect(page.getByTestId('chat-model-picker-menu')).toContainText('kimi-k2.7 (Moonshot)');
|
|
await expect(page.getByTestId('chat-model-picker-menu')).not.toContainText('kimi-k2.6 (Moonshot)');
|
|
await expect(page.getByTestId('chat-model-picker-menu')).not.toContainText('moonshot/kimi-k2.7 (Moonshot)');
|
|
await page.getByTestId('chat-model-picker-menu').getByRole('button', { name: 'provider/model-beta (Beta)' }).click();
|
|
await expect(page.getByTestId('chat-model-picker-button')).toContainText('provider/model-beta (Beta)');
|
|
|
|
const requests = await app.evaluate(() => (
|
|
(globalThis as typeof globalThis & { __chatModelPickerRequests?: Array<{ path: string; method: string; body: unknown }> }).__chatModelPickerRequests ?? []
|
|
));
|
|
expect(requests).toContainEqual({
|
|
path: '/api/agents/main/model',
|
|
method: 'PUT',
|
|
body: { modelRef: betaModelRef },
|
|
});
|
|
expect(requests.some((request) =>
|
|
request.path === '/api/gateway/restart'
|
|
|| request.path === '/api/gateway/start'
|
|
|| request.path === 'gateway:restart'
|
|
|| request.path === 'gateway:start'
|
|
|| request.path === 'gateway:config.patch'
|
|
)).toBe(false);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
});
|