feat: update desktop workflows and app center
This commit is contained in:
311
tests/unit/tasks-page.test.tsx
Normal file
311
tests/unit/tasks-page.test.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user