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

265 lines
9.8 KiB
TypeScript

/**
* @license
* Copyright 2025 AionUi (aionui.com)
* SPDX-License-Identifier: Apache-2.0
*/
import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IMcpServer } from '@/common/config/storage';
import { useGuidSend, type GuidSendDeps } from '@/renderer/pages/guid/hooks/useGuidSend';
const createConversationInvokeMock = vi.fn();
const swrMutateMock = vi.fn();
vi.mock('@/common', () => ({
ipcBridge: {
conversation: {
create: {
invoke: (...args: unknown[]) => createConversationInvokeMock(...args),
},
},
},
}));
vi.mock('@/renderer/utils/emitter', () => ({
emitter: {
emit: vi.fn(),
},
}));
vi.mock('swr', () => ({
mutate: (...args: unknown[]) => swrMutateMock(...args),
}));
vi.mock('@/renderer/utils/workspace/workspaceHistory', () => ({
updateWorkspaceTime: vi.fn(),
}));
vi.mock('@arco-design/web-react', () => ({
Message: {
warning: vi.fn(),
error: vi.fn(),
},
}));
const createDeps = (): GuidSendDeps => ({
input: 'hello',
setInput: vi.fn(),
files: [],
setFiles: vi.fn(),
dir: '',
setDir: vi.fn(),
setLoading: vi.fn(),
loading: false,
selectedAssistantId: 'assistant-1',
selectedAssistantBackend: 'claude',
selectedMode: 'bypassPermissions',
selectedAcpModel: 'claude-opus',
currentAcpCachedModelInfo: null,
current_model: undefined,
guidDisabledBuiltinSkills: undefined,
guidEnabledSkills: undefined,
assistantDefaultSkillIds: undefined,
assistantDefaultDisabledBuiltinSkillIds: undefined,
availableMcpServers: [{ id: 'mcp-user', name: 'User MCP', enabled: true, builtin: false } as IMcpServer],
selectedMcpServerIds: ['mcp-user'],
assistantDefaultMcpIds: undefined,
isGoogleAuth: false,
setMentionOpen: vi.fn(),
setMentionQuery: vi.fn(),
setMentionSelectorOpen: vi.fn(),
setMentionActiveIndex: vi.fn(),
navigate: vi.fn(() => Promise.resolve()) as never,
t: vi.fn((key: string, options?: { defaultValue?: string }) => options?.defaultValue || key) as never,
localeKey: 'zh-CN',
});
describe('useGuidSend', () => {
beforeEach(() => {
createConversationInvokeMock.mockReset();
createConversationInvokeMock.mockResolvedValue({ id: 'conv-1' });
swrMutateMock.mockReset();
swrMutateMock.mockResolvedValue(undefined);
});
it('passes selected mode into assistant conversation overrides when creating a preset ACP conversation', async () => {
const deps = createDeps();
(deps as any).selectedThoughtLevelValue = 'high';
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
expect(createConversationInvokeMock).toHaveBeenCalledTimes(1);
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.type).toBeUndefined();
expect('model' in payload).toBe(false);
expect(payload.assistant?.conversation_overrides?.permission).toBe('bypassPermissions');
expect(payload.assistant?.conversation_overrides?.model).toBe('claude-opus');
expect(payload.assistant?.conversation_overrides?.thought_level).toBe('high');
expect(payload.extra.backend).toBeUndefined();
expect(payload.extra.agent_name).toBeUndefined();
expect(payload.extra.agent_id).toBeUndefined();
expect(payload.extra.custom_agent_id).toBeUndefined();
expect(payload.extra.preset_rules).toBeUndefined();
expect(payload.extra.preset_context).toBeUndefined();
expect(payload.extra.session_mode).toBeUndefined();
expect(payload.extra.current_model_id).toBeUndefined();
expect(payload.extra.preset_assistant_id).toBeUndefined();
expect(swrMutateMock).toHaveBeenCalledWith('guid.assistant.detail.assistant-1.zh-CN');
expect(swrMutateMock).toHaveBeenCalledWith('assistants.list');
});
it('falls back to assistant default skill and MCP ids for preset conversations before local Guid overrides exist', async () => {
const deps = createDeps();
deps.guidEnabledSkills = undefined;
deps.guidDisabledBuiltinSkills = undefined;
deps.assistantDefaultSkillIds = ['assistant-skill'];
deps.assistantDefaultDisabledBuiltinSkillIds = ['builtin-skill'];
deps.selectedMcpServerIds = undefined;
deps.assistantDefaultMcpIds = ['mcp-user'];
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.assistant?.conversation_overrides?.skill_ids).toEqual(['assistant-skill']);
expect(payload.assistant?.conversation_overrides?.disabled_builtin_skill_ids).toEqual(['builtin-skill']);
expect(payload.assistant?.conversation_overrides?.mcp_ids).toEqual(['mcp-user']);
expect(payload.extra.selected_mcp_server_ids).toEqual(['mcp-user']);
});
it('preserves builtin MCP ids in assistant overrides while only sending user MCP ids to runtime selection', async () => {
const deps = createDeps();
deps.availableMcpServers = [
{ id: 'mcp-user', name: 'User MCP', enabled: true, builtin: false } as IMcpServer,
{ id: 'builtin-mcp', name: 'Builtin MCP', enabled: true, builtin: true } as IMcpServer,
];
deps.selectedMcpServerIds = ['mcp-user', 'builtin-mcp'];
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.assistant?.conversation_overrides?.mcp_ids).toEqual(['mcp-user', 'builtin-mcp']);
expect(payload.extra.selected_mcp_server_ids).toEqual(['mcp-user']);
expect(payload.extra.selected_session_mcp_servers).toEqual([expect.objectContaining({ id: 'builtin-mcp' })]);
});
it('does not write legacy preset_assistant_id for preset assistant sends', async () => {
const deps = createDeps();
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.assistant?.id).toBe('assistant-1');
expect(payload.extra.preset_assistant_id).toBeUndefined();
});
it('forwards local skill overrides through assistant conversation overrides for ACP assistants', async () => {
const deps = createDeps();
deps.guidEnabledSkills = ['pdf-reader'];
deps.guidDisabledBuiltinSkills = ['todo-tracker'];
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.assistant?.id).toBe('assistant-1');
expect(payload.assistant?.conversation_overrides?.skill_ids).toEqual(['pdf-reader']);
expect(payload.assistant?.conversation_overrides?.disabled_builtin_skill_ids).toEqual(['todo-tracker']);
});
it('forwards local skill overrides for generated Aion CLI assistants through assistant conversation overrides', async () => {
const deps = createDeps();
deps.selectedAssistantId = 'bare:aionrs';
deps.selectedAssistantBackend = 'aionrs';
deps.current_model = { provider_id: 'openai', model: 'gemini-2.5-pro', use_model: 'gemini-2.5-pro' } as never;
deps.guidEnabledSkills = ['pdf-reader'];
deps.guidDisabledBuiltinSkills = ['todo-tracker'];
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.type).toBeUndefined();
expect(payload.model).toBe(deps.current_model);
expect(payload.assistant?.id).toBe('bare:aionrs');
expect(payload.assistant?.conversation_overrides?.skill_ids).toEqual(['pdf-reader']);
expect(payload.assistant?.conversation_overrides?.disabled_builtin_skill_ids).toEqual(['todo-tracker']);
expect(payload.extra.session_mode).toBeUndefined();
});
it('does not write legacy preset_assistant_id for generated Aion CLI assistant conversations', async () => {
const deps = createDeps();
deps.selectedAssistantId = 'bare:aionrs';
deps.selectedAssistantBackend = 'aionrs';
deps.current_model = { provider_id: 'openai', model: 'gemini-2.5-pro', use_model: 'gemini-2.5-pro' } as never;
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.assistant?.id).toBe('bare:aionrs');
expect(payload.extra.preset_assistant_id).toBeUndefined();
});
it('does not write legacy preset_assistant_id for generated ACP assistant conversations', async () => {
const deps = createDeps();
deps.selectedAssistantId = 'bare:claude';
deps.selectedAssistantBackend = 'claude';
deps.current_model = { provider_id: 'anthropic', model: 'claude-sonnet', use_model: 'claude-sonnet' } as never;
const { result } = renderHook(() => useGuidSend(deps));
await act(async () => {
await result.current.handleSend();
});
const payload = createConversationInvokeMock.mock.calls[0][0];
expect(payload.assistant?.id).toBe('bare:claude');
expect(payload.type).toBeUndefined();
expect('model' in payload).toBe(false);
expect(payload.extra.preset_assistant_id).toBeUndefined();
expect(payload.extra.backend).toBeUndefined();
});
it('does not create a conversation without assistant identity', async () => {
const deps = createDeps();
deps.selectedAssistantId = null;
deps.selectedAssistantBackend = 'claude';
const { result } = renderHook(() => useGuidSend(deps));
expect(result.current.isButtonDisabled).toBe(true);
await act(async () => {
await result.current.handleSend();
});
expect(createConversationInvokeMock).not.toHaveBeenCalled();
});
});