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

243 lines
7.6 KiB
TypeScript

/**
* @license
* Copyright 2025 AionUi (aionui.com)
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { AgentLogoMap } from '@/renderer/utils/model/agentLogo';
import {
fetchAgentLogos,
resolveAgentAvatar,
resolveAgentLogo,
isDefaultModel,
getModelDisplayLabel,
} from '@/renderer/utils/model/agentLogo';
const bridgeMocks = vi.hoisted(() => ({
getManagedAgents: vi.fn(),
}));
vi.mock('@/common', () => ({
ipcBridge: {
acpConversation: {
getManagedAgents: { invoke: bridgeMocks.getManagedAgents },
},
},
}));
vi.mock('@/renderer/utils/platform', () => ({
resolveBackendAssetUrl: (url: string) => url,
}));
// Backend logo catalog returned by `useAgentLogos()` in production. The unit
// test passes it explicitly to the pure `resolveAgentLogo`.
const LOGOS: AgentLogoMap = {
claude: '/api/assets/logos/ai-major/claude.svg',
emojiagent: '🧠',
gemini: '/api/assets/logos/ai-major/gemini.svg',
opencode: '/api/assets/logos/tools/coding/opencode-light.svg',
'openclaw-gateway': '/api/assets/logos/tools/openclaw.svg',
};
describe('agentLogo', () => {
let originalDocument: Document | undefined;
beforeEach(() => {
bridgeMocks.getManagedAgents.mockReset();
if (typeof document !== 'undefined') {
originalDocument = document;
}
global.document = {
documentElement: {
getAttribute: vi.fn(() => 'light'),
},
} as any;
});
afterEach(() => {
if (originalDocument) {
global.document = originalDocument as any;
}
});
describe('resolveAgentLogo (backend lookup)', () => {
it('returns logo path for known backend (case-insensitive)', () => {
expect(resolveAgentLogo(LOGOS, { backend: 'Claude' })).toContain('/api/assets/logos/ai-major/claude.svg');
});
it('returns logo for lowercase input', () => {
expect(resolveAgentLogo(LOGOS, { backend: 'gemini' })).toContain('/api/assets/logos/ai-major/gemini.svg');
});
it('returns null for unknown backend', () => {
expect(resolveAgentLogo(LOGOS, { backend: 'unknown-agent' })).toBeNull();
});
it('returns null for null/undefined/empty backend', () => {
expect(resolveAgentLogo(LOGOS, { backend: null })).toBeNull();
expect(resolveAgentLogo(LOGOS, { backend: undefined })).toBeNull();
expect(resolveAgentLogo(LOGOS, { backend: '' })).toBeNull();
});
it('tolerates a missing catalog map', () => {
expect(resolveAgentLogo(undefined as unknown as AgentLogoMap, { backend: 'claude' })).toBeNull();
});
it('uses the backend-provided opencode logo without applying a theme variant', () => {
(global.document.documentElement.getAttribute as any).mockReturnValue('dark');
expect(resolveAgentLogo(LOGOS, { backend: 'opencode' })).toContain('opencode-light.svg');
});
it('does not expose local absolute paths as logo sources', () => {
expect(resolveAgentLogo(LOGOS, { icon: '/Users/demo/.aionui/agent-avatars/custom.png' })).toBeNull();
});
});
describe('resolveAgentAvatar', () => {
it('keeps explicit emoji avatars as emoji', () => {
expect(resolveAgentAvatar(LOGOS, { icon: '🧠', backend: 'claude' })).toEqual({
kind: 'emoji',
value: '🧠',
});
});
it('falls back to backend catalog logos when explicit local paths leak through', () => {
expect(
resolveAgentAvatar(LOGOS, { icon: '/Users/demo/.aionui/agent-avatars/custom.png', backend: 'claude' })
).toEqual({
kind: 'image',
value: '/api/assets/logos/ai-major/claude.svg',
});
});
it('keeps backend catalog emoji avatars as emoji', () => {
expect(resolveAgentAvatar(LOGOS, { backend: 'emojiagent' })).toEqual({
kind: 'emoji',
value: '🧠',
});
});
});
describe('fetchAgentLogos', () => {
it('builds the logo catalog from /api/agents/management rows', async () => {
bridgeMocks.getManagedAgents.mockResolvedValue([
{
id: 'agent-claude',
name: 'Claude',
agent_type: 'acp',
agent_source: 'builtin',
backend: 'claude',
enabled: true,
installed: true,
status: 'online',
icon: '/api/assets/logos/ai-major/claude.svg',
},
]);
await expect(fetchAgentLogos()).resolves.toEqual({
acp: '/api/assets/logos/ai-major/claude.svg',
'agent-claude': '/api/assets/logos/ai-major/claude.svg',
claude: '/api/assets/logos/ai-major/claude.svg',
});
expect(bridgeMocks.getManagedAgents).toHaveBeenCalledTimes(1);
});
});
describe('resolveAgentLogo (priority)', () => {
it('prioritizes explicit icon', () => {
expect(resolveAgentLogo(LOGOS, { icon: '/api/custom/icon.svg', backend: 'claude' })).toContain(
'/api/custom/icon.svg'
);
});
it('falls back to backend ID', () => {
expect(resolveAgentLogo(LOGOS, { backend: 'gemini' })).toContain('gemini.svg');
});
it('extracts adapter ID from custom_agent_id for extensions', () => {
expect(resolveAgentLogo(LOGOS, { isExtension: true, custom_agent_id: 'ext:my-ext:claude' })).toContain(
'claude.svg'
);
});
it('returns null when no match found', () => {
expect(resolveAgentLogo(LOGOS, { backend: 'unknown' })).toBeNull();
});
});
describe('isDefaultModel', () => {
it('returns true when value contains default', () => {
expect(isDefaultModel('gpt-4-default', null)).toBe(true);
});
it('returns true when label contains recommended', () => {
expect(isDefaultModel(null, 'recommended model')).toBe(true);
});
it('returns true when text contains 默认', () => {
expect(isDefaultModel('', '默认模型')).toBe(true);
});
it('returns false when no keywords present', () => {
expect(isDefaultModel('gpt-4', 'GPT-4')).toBe(false);
});
it('handles null inputs', () => {
expect(isDefaultModel(null, null)).toBe(false);
});
});
describe('getModelDisplayLabel', () => {
it('returns selectedLabel when provided and not default', () => {
const result = getModelDisplayLabel({
selected_value: 'gpt-4',
selectedLabel: 'GPT-4 Turbo',
defaultModelLabel: 'Default',
fallbackLabel: 'Unknown',
});
expect(result).toBe('GPT-4 Turbo');
});
it('keeps a specific model label even when it includes the default tier suffix', () => {
const result = getModelDisplayLabel({
selected_value: 'gpt-4',
selectedLabel: 'GPT-4 (default)',
defaultModelLabel: 'Default Model',
fallbackLabel: 'Unknown',
});
expect(result).toBe('GPT-4 (default)');
});
it('keeps a generic default option label unchanged', () => {
const result = getModelDisplayLabel({
selected_value: 'default/default',
selectedLabel: 'Default (default)',
defaultModelLabel: 'Default Model',
fallbackLabel: 'Unknown',
});
expect(result).toBe('Default (default)');
});
it('falls back to fallbackLabel when selectedLabel is null', () => {
const result = getModelDisplayLabel({
selected_value: 'gpt-4',
selectedLabel: null,
defaultModelLabel: 'Default',
fallbackLabel: 'Unnamed Model',
});
expect(result).toBe('Unnamed Model');
});
it('returns fallbackLabel when selectedLabel is empty', () => {
const result = getModelDisplayLabel({
selected_value: 'gpt-4',
selectedLabel: '',
defaultModelLabel: 'Default',
fallbackLabel: 'Fallback',
});
expect(result).toBe('Fallback');
});
});
});