Files
NianToB/tests/unit/tasks-page.test.tsx

312 lines
11 KiB
TypeScript

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> = {}): 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(
<MemoryRouter>
<Tasks />
</MemoryRouter>,
);
}
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',
});
});
});