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 { 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 { 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(); 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(); 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(); 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(); 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(); 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(); await waitFor(() => { expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith(hotelHangzhou.id); }); useYinianStore.setState({ config: createConfig({ hotel: hotelShanghai, }), }); rerender(); await waitFor(() => { expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith(hotelShanghai.id); }); }); });