265 lines
9.8 KiB
TypeScript
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();
|
|
});
|
|
});
|