Phase 12 - Performance Optimization: - Add message-virtualization.ts with useVirtualizedMessages hook - Implement MessageCache<T> LRU cache for rendered content - Add createMessageBatcher for WebSocket message batching - Add calculateVisibleRange and debounced scroll handlers - Support for 10,000+ messages without performance degradation Phase 13 - Test Coverage: - Add workflowStore.test.ts (28 tests) - Add configStore.test.ts (40 tests) - Update general-settings.test.tsx to match current UI - Total tests: 148 passing Code Quality: - TypeScript compilation passes - All 148 tests pass Co-Authored-By: Claude <noreply@anthropic.com>
735 lines
26 KiB
TypeScript
735 lines
26 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type {
|
|
ConfigStoreClient,
|
|
QuickConfig,
|
|
ChannelInfo,
|
|
SkillInfo,
|
|
ScheduledTask,
|
|
} from '../../../desktop/src/store/configStore';
|
|
|
|
// Mock client with all config methods
|
|
const createMockClient = (): ConfigStoreClient => ({
|
|
getWorkspaceInfo: vi.fn(),
|
|
getQuickConfig: vi.fn(),
|
|
saveQuickConfig: vi.fn(),
|
|
listSkills: vi.fn(),
|
|
getSkill: vi.fn(),
|
|
createSkill: vi.fn(),
|
|
updateSkill: vi.fn(),
|
|
deleteSkill: vi.fn(),
|
|
listChannels: vi.fn(),
|
|
getChannel: vi.fn(),
|
|
createChannel: vi.fn(),
|
|
updateChannel: vi.fn(),
|
|
deleteChannel: vi.fn(),
|
|
listScheduledTasks: vi.fn(),
|
|
createScheduledTask: vi.fn(),
|
|
listModels: vi.fn(),
|
|
getFeishuStatus: vi.fn(),
|
|
});
|
|
|
|
let mockClient: ConfigStoreClient;
|
|
|
|
function resetMocks() {
|
|
vi.clearAllMocks();
|
|
mockClient = createMockClient();
|
|
|
|
mockClient.getWorkspaceInfo = vi.fn().mockResolvedValue({
|
|
path: '~/.openfang/workspace',
|
|
resolvedPath: '/home/user/.openfang/workspace',
|
|
exists: true,
|
|
fileCount: 42,
|
|
totalSize: 1024000,
|
|
});
|
|
|
|
mockClient.getQuickConfig = vi.fn().mockResolvedValue({
|
|
quickConfig: {
|
|
agentName: 'ZCLAW',
|
|
theme: 'dark',
|
|
gatewayUrl: 'ws://127.0.0.1:4200/ws',
|
|
workspaceDir: '~/.openfang/workspace',
|
|
},
|
|
});
|
|
|
|
mockClient.saveQuickConfig = vi.fn().mockImplementation((config: QuickConfig) => ({
|
|
quickConfig: config,
|
|
}));
|
|
|
|
mockClient.listSkills = vi.fn().mockResolvedValue({
|
|
skills: [
|
|
{ id: 'builtin:translation', name: 'translation', path: '/skills/translation/SKILL.md', source: 'builtin' },
|
|
{ id: 'custom:summarize', name: 'summarize', path: '/custom/summarize/SKILL.md', source: 'extra' },
|
|
],
|
|
extraDirs: ['/custom-skills'],
|
|
});
|
|
|
|
mockClient.getSkill = vi.fn().mockImplementation((id: string) => ({
|
|
skill: { id, name: id.split(':')[1], path: `/skills/${id}/SKILL.md`, source: 'builtin' },
|
|
}));
|
|
|
|
mockClient.createSkill = vi.fn().mockImplementation((skill) => ({
|
|
skill: { id: `custom:${skill.name}`, ...skill, source: 'extra' as const, path: `/custom/${skill.name}/SKILL.md` },
|
|
}));
|
|
|
|
mockClient.updateSkill = vi.fn().mockImplementation((id: string, updates) => ({
|
|
skill: { id, name: updates.name || 'skill', path: `/skills/${id}/SKILL.md`, source: 'builtin' as const, ...updates },
|
|
}));
|
|
|
|
mockClient.deleteSkill = vi.fn().mockResolvedValue(undefined);
|
|
|
|
mockClient.listChannels = vi.fn().mockResolvedValue({
|
|
channels: [
|
|
{ id: 'feishu', type: 'feishu', label: 'Feishu', status: 'active', accounts: 2 },
|
|
{ id: 'slack', type: 'slack', label: 'Slack', status: 'inactive' },
|
|
],
|
|
});
|
|
|
|
mockClient.getChannel = vi.fn().mockImplementation((id: string) => ({
|
|
channel: { id, type: id, label: id.charAt(0).toUpperCase() + id.slice(1), status: 'active' as const },
|
|
}));
|
|
|
|
mockClient.createChannel = vi.fn().mockImplementation((channel) => ({
|
|
channel: { id: `channel_${Date.now()}`, ...channel, status: 'active' as const },
|
|
}));
|
|
|
|
mockClient.updateChannel = vi.fn().mockImplementation((id: string, updates) => ({
|
|
channel: { id, type: 'test', label: 'Test', status: 'active' as const, ...updates },
|
|
}));
|
|
|
|
mockClient.deleteChannel = vi.fn().mockResolvedValue(undefined);
|
|
|
|
mockClient.listScheduledTasks = vi.fn().mockResolvedValue({
|
|
tasks: [
|
|
{ id: 'task_1', name: 'Daily Report', schedule: '0 9 * * *', status: 'active' },
|
|
{ id: 'task_2', name: 'Weekly Backup', schedule: '0 0 * * 0', status: 'paused' },
|
|
],
|
|
});
|
|
|
|
mockClient.createScheduledTask = vi.fn().mockImplementation((task) => ({
|
|
id: `task_${Date.now()}`,
|
|
name: task.name,
|
|
schedule: task.schedule,
|
|
status: 'active' as const,
|
|
}));
|
|
|
|
mockClient.listModels = vi.fn().mockResolvedValue({
|
|
models: [
|
|
{ id: 'glm-4', name: 'GLM-4', provider: 'zhipuai' },
|
|
{ id: 'glm-5', name: 'GLM-5', provider: 'zhipuai' },
|
|
{ id: 'gpt-4', name: 'GPT-4', provider: 'openai' },
|
|
],
|
|
});
|
|
|
|
mockClient.getFeishuStatus = vi.fn().mockResolvedValue({ configured: true, accounts: 2 });
|
|
}
|
|
|
|
describe('configStore', () => {
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
resetMocks();
|
|
});
|
|
|
|
describe('initial state', () => {
|
|
it('initializes with default empty state', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.quickConfig).toEqual({});
|
|
expect(state.workspaceInfo).toBeNull();
|
|
expect(state.channels).toEqual([]);
|
|
expect(state.scheduledTasks).toEqual([]);
|
|
expect(state.skillsCatalog).toEqual([]);
|
|
expect(state.models).toEqual([]);
|
|
expect(state.modelsLoading).toBe(false);
|
|
expect(state.modelsError).toBeNull();
|
|
expect(state.error).toBeNull();
|
|
expect(state.client).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('client injection', () => {
|
|
it('accepts a client via setConfigStoreClient', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
expect(useConfigStore.getState().client).toBe(mockClient);
|
|
});
|
|
});
|
|
|
|
describe('loadQuickConfig', () => {
|
|
it('loads quick config from the client', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadQuickConfig();
|
|
|
|
expect(mockClient.getQuickConfig).toHaveBeenCalledTimes(1);
|
|
const state = useConfigStore.getState();
|
|
expect(state.quickConfig).toMatchObject({
|
|
agentName: 'ZCLAW',
|
|
theme: 'dark',
|
|
gatewayUrl: 'ws://127.0.0.1:4200/ws',
|
|
});
|
|
});
|
|
|
|
it('sets empty config when client returns null', async () => {
|
|
mockClient.getQuickConfig = vi.fn().mockResolvedValue(null);
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadQuickConfig();
|
|
|
|
expect(useConfigStore.getState().quickConfig).toEqual({});
|
|
});
|
|
|
|
it('handles errors silently', async () => {
|
|
mockClient.getQuickConfig = vi.fn().mockRejectedValue(new Error('Config error'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
// Should not throw
|
|
await expect(useConfigStore.getState().loadQuickConfig()).resolves.toBeUndefined();
|
|
expect(useConfigStore.getState().quickConfig).toEqual({});
|
|
});
|
|
|
|
it('does nothing when client is not set', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
await useConfigStore.getState().loadQuickConfig();
|
|
|
|
expect(mockClient.getQuickConfig).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('saveQuickConfig', () => {
|
|
it('merges updates with existing config and saves', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
useConfigStore.setState({
|
|
quickConfig: { agentName: 'ZCLAW', theme: 'dark' },
|
|
});
|
|
|
|
await useConfigStore.getState().saveQuickConfig({ theme: 'light', workspaceDir: '/new/path' });
|
|
|
|
expect(mockClient.saveQuickConfig).toHaveBeenCalledWith({
|
|
agentName: 'ZCLAW',
|
|
theme: 'light',
|
|
workspaceDir: '/new/path',
|
|
});
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.quickConfig.theme).toBe('light');
|
|
expect(state.quickConfig.workspaceDir).toBe('/new/path');
|
|
expect(state.quickConfig.agentName).toBe('ZCLAW');
|
|
});
|
|
|
|
it('sets error when save fails', async () => {
|
|
mockClient.saveQuickConfig = vi.fn().mockRejectedValue(new Error('Save failed'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
await useConfigStore.getState().saveQuickConfig({ theme: 'light' });
|
|
|
|
expect(useConfigStore.getState().error).toBe('Save failed');
|
|
});
|
|
|
|
it('does nothing when client is not set', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
await useConfigStore.getState().saveQuickConfig({ theme: 'light' });
|
|
|
|
expect(mockClient.saveQuickConfig).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('loadWorkspaceInfo', () => {
|
|
it('loads workspace info from the client', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadWorkspaceInfo();
|
|
|
|
expect(mockClient.getWorkspaceInfo).toHaveBeenCalledTimes(1);
|
|
const state = useConfigStore.getState();
|
|
expect(state.workspaceInfo).toMatchObject({
|
|
path: '~/.openfang/workspace',
|
|
resolvedPath: '/home/user/.openfang/workspace',
|
|
exists: true,
|
|
fileCount: 42,
|
|
totalSize: 1024000,
|
|
});
|
|
});
|
|
|
|
it('handles errors silently', async () => {
|
|
mockClient.getWorkspaceInfo = vi.fn().mockRejectedValue(new Error('Workspace error'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
await expect(useConfigStore.getState().loadWorkspaceInfo()).resolves.toBeUndefined();
|
|
expect(useConfigStore.getState().workspaceInfo).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('loadChannels', () => {
|
|
it('loads channels from the client', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadChannels();
|
|
|
|
expect(mockClient.listChannels).toHaveBeenCalledTimes(1);
|
|
const state = useConfigStore.getState();
|
|
expect(state.channels).toHaveLength(2);
|
|
expect(state.channels[0]).toMatchObject({
|
|
id: 'feishu',
|
|
type: 'feishu',
|
|
label: 'Feishu',
|
|
status: 'active',
|
|
accounts: 2,
|
|
});
|
|
});
|
|
|
|
it('falls back to probing feishu status when listChannels fails', async () => {
|
|
mockClient.listChannels = vi.fn().mockRejectedValue(new Error('Channels unavailable'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadChannels();
|
|
|
|
expect(mockClient.getFeishuStatus).toHaveBeenCalledTimes(1);
|
|
const state = useConfigStore.getState();
|
|
expect(state.channels).toHaveLength(1);
|
|
expect(state.channels[0]).toMatchObject({
|
|
id: 'feishu',
|
|
type: 'feishu',
|
|
status: 'active',
|
|
});
|
|
});
|
|
|
|
it('handles feishu status errors gracefully', async () => {
|
|
mockClient.listChannels = vi.fn().mockRejectedValue(new Error('Channels unavailable'));
|
|
mockClient.getFeishuStatus = vi.fn().mockRejectedValue(new Error('Feishu error'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadChannels();
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.channels).toHaveLength(1);
|
|
expect(state.channels[0]).toMatchObject({
|
|
id: 'feishu',
|
|
status: 'inactive',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getChannel', () => {
|
|
it('fetches and returns a single channel', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
const channel = await useConfigStore.getState().getChannel('feishu');
|
|
|
|
expect(mockClient.getChannel).toHaveBeenCalledWith('feishu');
|
|
expect(channel).toMatchObject({
|
|
id: 'feishu',
|
|
status: 'active',
|
|
});
|
|
});
|
|
|
|
it('updates existing channel in state', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
useConfigStore.setState({
|
|
channels: [{ id: 'feishu', type: 'feishu', label: 'Feishu', status: 'inactive' }],
|
|
});
|
|
|
|
await useConfigStore.getState().getChannel('feishu');
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.channels[0].status).toBe('active');
|
|
});
|
|
|
|
it('sets error on failure', async () => {
|
|
mockClient.getChannel = vi.fn().mockRejectedValue(new Error('Channel error'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
const result = await useConfigStore.getState().getChannel('unknown');
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(useConfigStore.getState().error).toBe('Channel error');
|
|
});
|
|
});
|
|
|
|
describe('createChannel', () => {
|
|
it('creates a channel and adds it to state', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
const channel = await useConfigStore.getState().createChannel({
|
|
type: 'discord',
|
|
name: 'Discord Bot',
|
|
config: { webhook: 'https://discord.com/...' },
|
|
enabled: true,
|
|
});
|
|
|
|
expect(mockClient.createChannel).toHaveBeenCalled();
|
|
expect(channel).toBeDefined();
|
|
expect(channel?.status).toBe('active');
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.channels).toHaveLength(1);
|
|
});
|
|
|
|
it('sets error on create failure', async () => {
|
|
mockClient.createChannel = vi.fn().mockRejectedValue(new Error('Create failed'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
const result = await useConfigStore.getState().createChannel({
|
|
type: 'test',
|
|
name: 'Test',
|
|
config: {},
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(useConfigStore.getState().error).toBe('Create failed');
|
|
});
|
|
});
|
|
|
|
describe('updateChannel', () => {
|
|
it('updates a channel in state', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
useConfigStore.setState({
|
|
channels: [{ id: 'feishu', type: 'feishu', label: 'Feishu', status: 'inactive' }],
|
|
});
|
|
|
|
const result = await useConfigStore.getState().updateChannel('feishu', {
|
|
name: 'Updated Feishu',
|
|
enabled: true,
|
|
});
|
|
|
|
expect(mockClient.updateChannel).toHaveBeenCalledWith('feishu', expect.objectContaining({ name: 'Updated Feishu' }));
|
|
expect(result).toBeDefined();
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.channels[0].status).toBe('active');
|
|
});
|
|
|
|
it('sets error on update failure', async () => {
|
|
mockClient.updateChannel = vi.fn().mockRejectedValue(new Error('Update failed'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
const result = await useConfigStore.getState().updateChannel('unknown', { name: 'Test' });
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(useConfigStore.getState().error).toBe('Update failed');
|
|
});
|
|
});
|
|
|
|
describe('deleteChannel', () => {
|
|
it('deletes a channel from state', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
useConfigStore.setState({
|
|
channels: [
|
|
{ id: 'feishu', type: 'feishu', label: 'Feishu', status: 'active' },
|
|
{ id: 'slack', type: 'slack', label: 'Slack', status: 'inactive' },
|
|
],
|
|
});
|
|
|
|
await useConfigStore.getState().deleteChannel('feishu');
|
|
|
|
expect(mockClient.deleteChannel).toHaveBeenCalledWith('feishu');
|
|
const state = useConfigStore.getState();
|
|
expect(state.channels).toHaveLength(1);
|
|
expect(state.channels.find(c => c.id === 'feishu')).toBeUndefined();
|
|
});
|
|
|
|
it('sets error on delete failure', async () => {
|
|
mockClient.deleteChannel = vi.fn().mockRejectedValue(new Error('Delete failed'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
await useConfigStore.getState().deleteChannel('unknown');
|
|
|
|
expect(useConfigStore.getState().error).toBe('Delete failed');
|
|
});
|
|
});
|
|
|
|
describe('loadSkillsCatalog', () => {
|
|
it('loads skills and extra dirs from the client', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadSkillsCatalog();
|
|
|
|
expect(mockClient.listSkills).toHaveBeenCalledTimes(1);
|
|
const state = useConfigStore.getState();
|
|
expect(state.skillsCatalog).toHaveLength(2);
|
|
expect(state.skillsCatalog[0]).toMatchObject({
|
|
id: 'builtin:translation',
|
|
name: 'translation',
|
|
source: 'builtin',
|
|
});
|
|
expect(state.quickConfig.skillsExtraDirs).toEqual(['/custom-skills']);
|
|
});
|
|
|
|
it('handles errors silently', async () => {
|
|
mockClient.listSkills = vi.fn().mockRejectedValue(new Error('Skills error'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
await expect(useConfigStore.getState().loadSkillsCatalog()).resolves.toBeUndefined();
|
|
expect(useConfigStore.getState().skillsCatalog).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getSkill', () => {
|
|
it('fetches a single skill by id', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
const skill = await useConfigStore.getState().getSkill('builtin:translation');
|
|
|
|
expect(mockClient.getSkill).toHaveBeenCalledWith('builtin:translation');
|
|
expect(skill).toMatchObject({
|
|
id: 'builtin:translation',
|
|
name: 'translation',
|
|
});
|
|
});
|
|
|
|
it('returns undefined on error', async () => {
|
|
mockClient.getSkill = vi.fn().mockRejectedValue(new Error('Skill not found'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
const result = await useConfigStore.getState().getSkill('unknown');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('createSkill', () => {
|
|
it('creates a skill and adds it to catalog', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
const skill = await useConfigStore.getState().createSkill({
|
|
name: 'analyzer',
|
|
description: 'Analyzes content',
|
|
triggers: [{ type: 'keyword', pattern: 'analyze' }],
|
|
actions: [{ type: 'hand', params: { hand: 'analyzer' } }],
|
|
enabled: true,
|
|
});
|
|
|
|
expect(mockClient.createSkill).toHaveBeenCalled();
|
|
expect(skill).toBeDefined();
|
|
expect(skill?.id).toBe('custom:analyzer');
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.skillsCatalog).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('updateSkill', () => {
|
|
it('updates a skill in catalog', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
useConfigStore.setState({
|
|
skillsCatalog: [{ id: 'builtin:test', name: 'test', path: '/test', source: 'builtin' }],
|
|
});
|
|
|
|
const result = await useConfigStore.getState().updateSkill('builtin:test', {
|
|
name: 'updated',
|
|
description: 'Updated skill',
|
|
});
|
|
|
|
expect(mockClient.updateSkill).toHaveBeenCalledWith('builtin:test', expect.objectContaining({ name: 'updated' }));
|
|
expect(result).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('deleteSkill', () => {
|
|
it('deletes a skill from catalog', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
useConfigStore.setState({
|
|
skillsCatalog: [
|
|
{ id: 'skill_1', name: 'Skill 1', path: '/s1', source: 'builtin' },
|
|
{ id: 'skill_2', name: 'Skill 2', path: '/s2', source: 'builtin' },
|
|
],
|
|
});
|
|
|
|
await useConfigStore.getState().deleteSkill('skill_1');
|
|
|
|
expect(mockClient.deleteSkill).toHaveBeenCalledWith('skill_1');
|
|
const state = useConfigStore.getState();
|
|
expect(state.skillsCatalog).toHaveLength(1);
|
|
expect(state.skillsCatalog.find(s => s.id === 'skill_1')).toBeUndefined();
|
|
});
|
|
|
|
it('handles errors silently', async () => {
|
|
mockClient.deleteSkill = vi.fn().mockRejectedValue(new Error('Delete failed'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
useConfigStore.setState({
|
|
skillsCatalog: [{ id: 'skill_1', name: 'Skill 1', path: '/s1', source: 'builtin' }],
|
|
});
|
|
|
|
// Should not throw
|
|
await expect(useConfigStore.getState().deleteSkill('skill_1')).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('loadScheduledTasks', () => {
|
|
it('loads scheduled tasks from the client', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadScheduledTasks();
|
|
|
|
expect(mockClient.listScheduledTasks).toHaveBeenCalledTimes(1);
|
|
const state = useConfigStore.getState();
|
|
expect(state.scheduledTasks).toHaveLength(2);
|
|
expect(state.scheduledTasks[0]).toMatchObject({
|
|
id: 'task_1',
|
|
name: 'Daily Report',
|
|
schedule: '0 9 * * *',
|
|
status: 'active',
|
|
});
|
|
});
|
|
|
|
it('handles errors silently', async () => {
|
|
mockClient.listScheduledTasks = vi.fn().mockRejectedValue(new Error('Tasks error'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
await expect(useConfigStore.getState().loadScheduledTasks()).resolves.toBeUndefined();
|
|
expect(useConfigStore.getState().scheduledTasks).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('createScheduledTask', () => {
|
|
it('creates a scheduled task and adds to state', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
const task = await useConfigStore.getState().createScheduledTask({
|
|
name: 'New Task',
|
|
schedule: '0 10 * * *',
|
|
scheduleType: 'cron',
|
|
target: { type: 'hand', id: 'echo' },
|
|
description: 'A new task',
|
|
enabled: true,
|
|
});
|
|
|
|
expect(mockClient.createScheduledTask).toHaveBeenCalled();
|
|
expect(task).toBeDefined();
|
|
expect(task?.status).toBe('active');
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.scheduledTasks).toHaveLength(1);
|
|
});
|
|
|
|
it('sets error on create failure', async () => {
|
|
mockClient.createScheduledTask = vi.fn().mockRejectedValue(new Error('Create task failed'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
const result = await useConfigStore.getState().createScheduledTask({
|
|
name: 'Failed Task',
|
|
schedule: 'invalid',
|
|
scheduleType: 'cron',
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(useConfigStore.getState().error).toBe('Create task failed');
|
|
});
|
|
});
|
|
|
|
describe('loadModels', () => {
|
|
it('loads models from the client', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadModels();
|
|
|
|
expect(mockClient.listModels).toHaveBeenCalledTimes(1);
|
|
const state = useConfigStore.getState();
|
|
expect(state.models).toHaveLength(3);
|
|
expect(state.modelsLoading).toBe(false);
|
|
expect(state.modelsError).toBeNull();
|
|
});
|
|
|
|
it('sets modelsLoading during load', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
|
|
const loadPromise = useConfigStore.getState().loadModels();
|
|
await loadPromise;
|
|
|
|
expect(useConfigStore.getState().modelsLoading).toBe(false);
|
|
});
|
|
|
|
it('sets modelsError when load fails', async () => {
|
|
mockClient.listModels = vi.fn().mockRejectedValue(new Error('Models error'));
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadModels();
|
|
|
|
const state = useConfigStore.getState();
|
|
expect(state.modelsError).toBe('Models error');
|
|
expect(state.modelsLoading).toBe(false);
|
|
expect(state.models).toEqual([]);
|
|
});
|
|
|
|
it('handles null result from client', async () => {
|
|
mockClient.listModels = vi.fn().mockResolvedValue({ models: null });
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.getState().setConfigStoreClient(mockClient);
|
|
await useConfigStore.getState().loadModels();
|
|
|
|
expect(useConfigStore.getState().models).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('clearError', () => {
|
|
it('clears the error state', async () => {
|
|
const { useConfigStore } = await import('../../../desktop/src/store/configStore');
|
|
|
|
useConfigStore.setState({ error: 'Some error' });
|
|
useConfigStore.getState().clearError();
|
|
|
|
expect(useConfigStore.getState().error).toBeNull();
|
|
});
|
|
});
|
|
});
|