265 lines
8.4 KiB
TypeScript
265 lines
8.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { YinianSkills } from '@/pages/YinianSkills';
|
|
import { useYinianStore } from '@/stores/yinian';
|
|
import { useYinianSkillsStore } from '@/stores/yinian-skills';
|
|
import { useSkillsStore } from '@/stores/skills';
|
|
import type { YinianConfigSnapshot, YinianLocalSkill } from '../../shared/yinian';
|
|
|
|
const toastSuccessMock = vi.fn();
|
|
const toastErrorMock = vi.fn();
|
|
|
|
vi.mock('sonner', () => ({
|
|
toast: {
|
|
success: (...args: unknown[]) => toastSuccessMock(...args),
|
|
error: (...args: unknown[]) => toastErrorMock(...args),
|
|
},
|
|
}));
|
|
|
|
const hotelHangzhou = {
|
|
id: 'workspace_hangzhou_ops',
|
|
name: '智念企业组织空间',
|
|
city: '杭州',
|
|
role: 'manager' as const,
|
|
permissions: ['skills.sync'],
|
|
ota: [],
|
|
};
|
|
|
|
const hotelShanghai = {
|
|
id: 'workspace_shanghai_growth',
|
|
name: '智念增长组织空间',
|
|
city: '上海',
|
|
role: 'viewer' as const,
|
|
permissions: ['skills.read'],
|
|
ota: [],
|
|
};
|
|
|
|
function createConfig(overrides: Partial<YinianConfigSnapshot> = {}): YinianConfigSnapshot {
|
|
return {
|
|
serverTime: 1,
|
|
user: { id: 'user_1', name: '王管理员' },
|
|
hotel: hotelHangzhou,
|
|
hotels: [hotelHangzhou, hotelShanghai],
|
|
entitlements: [],
|
|
notificationChannels: [],
|
|
featureFlags: {},
|
|
uiPolicy: { defaultPage: 'today', showAdvancedSettings: false },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createLocalSkill(overrides: Partial<YinianLocalSkill>): YinianLocalSkill {
|
|
return {
|
|
skillId: 'daily-report',
|
|
name: '日报生成助手',
|
|
version: '1.0.0',
|
|
enabled: true,
|
|
installedAt: 1,
|
|
lastSyncedAt: 2,
|
|
status: 'installed',
|
|
source: 'mock',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('YinianSkills page', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
useYinianStore.setState({
|
|
status: 'authenticated',
|
|
session: {
|
|
authenticated: true,
|
|
user: { id: 'user_1', name: '王管理员' },
|
|
hotels: [hotelHangzhou, hotelShanghai],
|
|
currentHotelId: hotelHangzhou.id,
|
|
accessTokenExpiresAt: 100,
|
|
},
|
|
config: createConfig(),
|
|
error: null,
|
|
});
|
|
useYinianSkillsStore.setState({
|
|
localSkills: [],
|
|
lastSync: null,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
useSkillsStore.setState({
|
|
skills: [],
|
|
loading: false,
|
|
error: null,
|
|
fetchSkills: vi.fn().mockResolvedValue(undefined),
|
|
});
|
|
vi.mocked(window.yinian.skills.getRegistry).mockReset();
|
|
vi.mocked(window.yinian.skills.getRegistry).mockResolvedValue(undefined);
|
|
vi.mocked(window.yinian.skills.sync).mockReset();
|
|
});
|
|
|
|
it('shows empty entitlement state', () => {
|
|
render(<YinianSkills />);
|
|
|
|
expect(screen.getByText('智念企业组织空间')).toBeInTheDocument();
|
|
expect(screen.getByTestId('yinian-skills-empty-entitlements')).toHaveTextContent('当前组织空间尚未开通应用');
|
|
});
|
|
|
|
it('maps entitlement and registry states for installed, update, disabled, and failed skills', () => {
|
|
useYinianStore.setState({
|
|
config: createConfig({
|
|
entitlements: [
|
|
{
|
|
skillId: 'daily-report',
|
|
name: '日报生成助手',
|
|
version: '1.0.0',
|
|
enabled: true,
|
|
category: 'reporting',
|
|
triggers: ['scheduled'],
|
|
},
|
|
{
|
|
skillId: 'data-check',
|
|
name: '数据检查助手',
|
|
version: '1.2.0',
|
|
enabled: true,
|
|
category: 'ota-monitoring',
|
|
triggers: ['manual'],
|
|
},
|
|
{
|
|
skillId: 'customer-reply-helper',
|
|
name: '客户回复助手',
|
|
version: '0.9.0',
|
|
enabled: false,
|
|
category: 'guest-comm',
|
|
triggers: ['manual'],
|
|
},
|
|
{
|
|
skillId: 'ops-check',
|
|
name: '流程检查',
|
|
version: '0.1.0',
|
|
enabled: true,
|
|
category: 'ops-automation',
|
|
triggers: [],
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
useYinianSkillsStore.setState({
|
|
localSkills: [
|
|
createLocalSkill({ skillId: 'daily-report', name: '日报生成助手', version: '1.0.0', status: 'skipped' }),
|
|
createLocalSkill({ skillId: 'data-check', name: '数据检查助手', version: '1.1.0', status: 'installed', source: 'nianxx', bundleSha256: 'sha256-demo' }),
|
|
createLocalSkill({ skillId: 'ops-check', name: '流程检查', version: '0.1.0', status: 'failed', error: 'download failed' }),
|
|
],
|
|
});
|
|
|
|
render(<YinianSkills />);
|
|
|
|
expect(screen.getByTestId('yinian-skill-state-daily-report')).toHaveTextContent('已是最新');
|
|
expect(screen.getByTestId('yinian-skill-state-data-check')).toHaveTextContent('有更新');
|
|
expect(screen.getByTestId('yinian-skill-state-customer-reply-helper')).toHaveTextContent('已禁用');
|
|
expect(screen.getByTestId('yinian-skill-state-ops-check')).toHaveTextContent('同步失败');
|
|
fireEvent.click(screen.getByRole('button', { name: /流程检查/ }));
|
|
expect(screen.getByText('download failed')).toBeInTheDocument();
|
|
expect(screen.getAllByText('版本校验').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('shows locally installed OpenClaw skills in a separate tab', () => {
|
|
useSkillsStore.setState({
|
|
skills: [
|
|
{
|
|
id: 'local-summary',
|
|
slug: 'local-summary',
|
|
name: '本地总结助手',
|
|
description: '整理本地会话内容',
|
|
enabled: true,
|
|
version: '0.2.0',
|
|
source: 'openclaw-managed',
|
|
baseDir: '/tmp/openclaw/skills/local-summary',
|
|
},
|
|
],
|
|
loading: false,
|
|
error: null,
|
|
fetchSkills: vi.fn().mockResolvedValue(undefined),
|
|
});
|
|
|
|
render(<YinianSkills />);
|
|
fireEvent.click(screen.getByRole('button', { name: /本地安装/ }));
|
|
|
|
expect(screen.getByText('本地总结助手')).toBeInTheDocument();
|
|
expect(screen.getByText('整理本地会话内容')).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: /本地总结助手/ }));
|
|
expect(screen.getByText('/tmp/openclaw/skills/local-summary')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows empty registry warning when entitlements exist but no local skills are installed', () => {
|
|
useYinianStore.setState({
|
|
config: createConfig({
|
|
entitlements: [
|
|
{
|
|
skillId: 'daily-report',
|
|
name: '日报生成助手',
|
|
version: '1.0.0',
|
|
enabled: true,
|
|
category: 'reporting',
|
|
triggers: ['scheduled'],
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
|
|
render(<YinianSkills />);
|
|
|
|
expect(screen.getByTestId('yinian-skill-state-daily-report')).toHaveTextContent('已开通未同步');
|
|
expect(screen.getByTestId('yinian-skills-empty-registry')).toHaveTextContent('这台电脑还没有准备好应用');
|
|
});
|
|
|
|
it('sync button stores result and shows success toast', async () => {
|
|
useYinianStore.setState({
|
|
config: createConfig({
|
|
entitlements: [
|
|
{
|
|
skillId: 'daily-report',
|
|
name: '日报生成助手',
|
|
version: '1.0.0',
|
|
enabled: true,
|
|
category: 'reporting',
|
|
triggers: ['scheduled'],
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
vi.mocked(window.yinian.skills.sync).mockResolvedValueOnce({
|
|
hotelId: hotelHangzhou.id,
|
|
syncedAt: 10,
|
|
skills: [createLocalSkill({ skillId: 'daily-report', name: '日报生成助手', version: '1.0.0', status: 'installed' })],
|
|
});
|
|
|
|
render(<YinianSkills />);
|
|
const syncButton = screen.getByRole('button', { name: '同步应用' });
|
|
await waitFor(() => {
|
|
expect(syncButton).not.toBeDisabled();
|
|
});
|
|
fireEvent.click(syncButton);
|
|
|
|
await waitFor(() => {
|
|
expect(window.yinian.skills.sync).toHaveBeenCalled();
|
|
expect(toastSuccessMock).toHaveBeenCalledWith('应用已同步');
|
|
});
|
|
expect(useYinianSkillsStore.getState().localSkills).toHaveLength(1);
|
|
});
|
|
|
|
it('loads registry for the current workspace when workspace changes', async () => {
|
|
const { rerender } = render(<YinianSkills />);
|
|
await waitFor(() => {
|
|
expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith(hotelHangzhou.id);
|
|
});
|
|
|
|
useYinianStore.setState({
|
|
config: createConfig({
|
|
hotel: hotelShanghai,
|
|
}),
|
|
});
|
|
rerender(<YinianSkills />);
|
|
|
|
await waitFor(() => {
|
|
expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith(hotelShanghai.id);
|
|
});
|
|
});
|
|
});
|