import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { Tasks } from '@/pages/Tasks'; import { useCronStore } from '@/stores/cron'; import { useGatewayStore } from '@/stores/gateway'; import { useSkillsStore } from '@/stores/skills'; import { useTaskCenterStore } from '@/stores/task-center'; import { useYinianStore } from '@/stores/yinian'; import { useChatStore } from '@/stores/chat'; import i18n from '@/i18n'; import type { YinianConfigSnapshot } from '../../shared/yinian'; const hostApiFetchMock = vi.fn(); const invokeIpcMock = vi.fn(); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), })); vi.mock('@/lib/api-client', () => ({ invokeIpc: (...args: unknown[]) => invokeIpcMock(...args), })); const hotelHangzhou = { id: 'workspace_hangzhou_ops', name: '智念企业组织空间', city: '杭州', role: 'manager' as const, permissions: ['skills.sync'], ota: [], }; function createConfig(overrides: Partial = {}): YinianConfigSnapshot { return { serverTime: 1, user: { id: 'user_1', name: '王管理员' }, hotel: hotelHangzhou, hotels: [hotelHangzhou], entitlements: [ { skillId: 'daily-report', name: '日报生成助手', version: '1.0.0', enabled: true, category: 'reporting', triggers: ['manual', 'scheduled'], }, { skillId: 'weekly-report', name: '周报分析助手', version: '1.0.0', enabled: true, category: 'reporting', triggers: ['manual', 'scheduled'], }, ], notificationChannels: [], featureFlags: {}, uiPolicy: { defaultPage: 'today', showAdvancedSettings: false }, ...overrides, }; } function renderTasks() { return render( , ); } describe('Tasks page', () => { beforeEach(async () => { vi.clearAllMocks(); await i18n.changeLanguage('zh'); invokeIpcMock.mockResolvedValue({ success: true, result: { messages: [] } }); hostApiFetchMock.mockImplementation(async (path: string, options?: { method?: string; body?: string }) => { if (path === '/api/channels/accounts') { return { success: true, channels: [ { channelType: 'feishu', defaultAccountId: 'feishu-main', accounts: [{ accountId: 'feishu-main', name: '飞书主账号', isDefault: true }], }, ], }; } if (path.startsWith('/api/channels/targets')) { return { success: true, targets: [{ value: 'user:ou_daily', label: '运营负责人', kind: 'user' }], }; } if (path === '/api/cron/jobs' && options?.method === 'POST') { const body = JSON.parse(options.body || '{}'); return { id: 'cron-daily', createdAt: '2026-05-13T00:00:00.000Z', updatedAt: '2026-05-13T00:00:00.000Z', nextRun: '2026-05-14T01:00:00.000Z', lastRun: undefined, agentId: body.agentId || 'main', ...body, }; } if (path === '/api/cron/jobs') { return { jobs: [] }; } return { success: true }; }); useYinianStore.setState({ status: 'authenticated', session: { authenticated: true, user: { id: 'user_1', name: '王管理员' }, hotels: [hotelHangzhou], currentHotelId: hotelHangzhou.id, accessTokenExpiresAt: 100, }, config: createConfig(), error: null, }); useGatewayStore.setState({ status: { state: 'running', port: 18789, gatewayReady: true, }, }); useSkillsStore.setState({ skills: [], loading: false, error: null, fetchSkills: vi.fn().mockResolvedValue(undefined), }); useCronStore.setState({ jobs: [], loading: false, error: null, }); useTaskCenterStore.setState({ scheduledBindings: {}, runRecords: [], pinnedTaskIds: [], }); useChatStore.setState({ currentSessionKey: 'agent:main:main', currentAgentId: 'main', sessions: [{ key: 'agent:main:main', displayName: 'main' }], messages: [], sessionLabels: {}, sessionLastActivity: {}, sending: false, activeRunId: null, activeRunSessionKey: null, streamingText: '', streamingMessage: null, streamingTools: [], pendingFinal: false, lastUserMessageAt: null, pendingToolImages: [], error: null, }); }); it('offers quick task templates that prefill the scheduled task dialog', async () => { renderTasks(); expect(screen.getByTestId('tasks-quick-templates')).toHaveTextContent('快捷任务'); fireEvent.click(screen.getByTestId('tasks-template-daily-brief')); expect(screen.getByDisplayValue('每日经营日报')).toBeInTheDocument(); expect(screen.getByDisplayValue(/昨日经营情况/)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith('/api/cron/jobs', expect.objectContaining({ method: 'POST' })); }); const createCall = hostApiFetchMock.mock.calls.find(([path, options]) => ( path === '/api/cron/jobs' && (options as { method?: string } | undefined)?.method === 'POST' )); const payload = JSON.parse((createCall?.[1] as { body: string }).body); expect(payload).toMatchObject({ name: '每日经营日报', schedule: '0 9 * * *', enabled: true, }); expect(payload.message).toContain('昨日经营情况'); }); it('pins task center cards to the sidebar quick trigger list', async () => { useCronStore.setState({ jobs: [{ id: 'cron-daily', name: '每日经营日报', message: '使用日报生成助手 skill 生成日报', schedule: '0 9 * * *', delivery: { mode: 'none' }, enabled: true, createdAt: '2026-05-13T00:00:00.000Z', updatedAt: '2026-05-13T00:00:00.000Z', agentId: 'main', }], loading: false, error: null, }); renderTasks(); fireEvent.click(await screen.findByRole('button', { name: '钉到侧边栏' })); expect(useTaskCenterStore.getState().pinnedTaskIds).toEqual(['cron-daily']); fireEvent.click(screen.getByRole('button', { name: '取消固定' })); expect(useTaskCenterStore.getState().pinnedTaskIds).toEqual([]); }); it('runs a task in its fixed conversation and switches there immediately', async () => { useCronStore.setState({ jobs: [{ id: 'cron-daily', name: '每日经营日报', message: '使用日报生成助手 skill 生成日报', schedule: '0 9 * * *', delivery: { mode: 'none' }, enabled: true, createdAt: '2026-05-13T00:00:00.000Z', updatedAt: '2026-05-13T00:00:00.000Z', agentId: 'main', }], loading: false, error: null, }); renderTasks(); fireEvent.click(await screen.findByRole('button', { name: '立即执行' })); await waitFor(() => { expect(useChatStore.getState().currentSessionKey).toBe('agent:main:cron:cron-daily'); }); expect(useChatStore.getState().sessionLabels['agent:main:cron:cron-daily']).toBe('每日经营日报'); expect(useChatStore.getState().sessions.find((session) => session.key === 'agent:main:cron:cron-daily')).toMatchObject({ label: '每日经营日报', displayName: '每日经营日报', }); expect(useTaskCenterStore.getState().runRecords[0]).toMatchObject({ cronJobId: 'cron-daily', sessionKey: 'agent:main:cron:cron-daily', }); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/cron/trigger', expect.objectContaining({ method: 'POST' })); }); it('creates task center tasks with keyboard @ skill insertion, structured custom time, and delivery channel payload', async () => { renderTasks(); expect(screen.queryByTestId('tasks-tab-quick')).not.toBeInTheDocument(); fireEvent.click(await screen.findByRole('button', { name: '新建任务' })); expect(screen.queryByText('快捷能力模板(可选)')).not.toBeInTheDocument(); fireEvent.change(screen.getByPlaceholderText('例如:每日经营日报'), { target: { value: '每日经营日报' }, }); const contentBox = screen.getByPlaceholderText('写清楚每次执行时需要完成的具体内容'); fireEvent.change(contentBox, { target: { value: '@' } }); await screen.findByRole('option', { name: /日报生成助手/ }); fireEvent.keyDown(contentBox, { key: 'ArrowDown' }); fireEvent.keyDown(contentBox, { key: 'Enter' }); expect(contentBox).toHaveValue('使用周报分析助手 skill'); const middleText = '请先检查@日报后继续'; fireEvent.change(contentBox, { target: { value: middleText, selectionStart: '请先检查@日报'.length, }, }); fireEvent.mouseDown(await screen.findByRole('option', { name: /日报生成助手/ })); expect(contentBox).toHaveValue('请先检查 使用日报生成助手 skill 后继续'); fireEvent.click(screen.getByRole('button', { name: '自定义时间' })); fireEvent.change(screen.getByLabelText('重复方式'), { target: { value: 'weekly' } }); fireEvent.change(screen.getByLabelText('执行时刻'), { target: { value: '10:30' } }); fireEvent.change(screen.getByLabelText('星期'), { target: { value: '3' } }); fireEvent.click(screen.getByRole('button', { name: /发送到外部通道/ })); fireEvent.change(screen.getByLabelText('通道'), { target: { value: 'feishu' } }); await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/channels/targets?channelType=feishu&accountId=feishu-main')); fireEvent.change(screen.getByLabelText('接收目标'), { target: { value: 'user:ou_daily' } }); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith('/api/cron/jobs', expect.objectContaining({ method: 'POST' })); }); const createCall = hostApiFetchMock.mock.calls.find(([path, options]) => ( path === '/api/cron/jobs' && (options as { method?: string } | undefined)?.method === 'POST' )); const payload = JSON.parse((createCall?.[1] as { body: string }).body); expect(payload.message).toContain('请先检查 使用日报生成助手 skill 后继续'); expect(payload.schedule).toBe('30 10 * * 3'); expect(payload.delivery).toEqual({ mode: 'announce', channel: 'feishu', accountId: 'feishu-main', to: 'user:ou_daily', }); }); });