Add unit tests for skill capabilities, skill planner, and UV setup

- Implement tests for random ID generation, ensuring preference for crypto.randomUUID.
- Create tests for runtime context capabilities, validating the injection of enabled skill capabilities.
- Add tests for skill capability parsing, including classification and command example extraction.
- Introduce tests for the skill planner, verifying tool call planning based on user requests and attachment requirements.
- Establish tests for UV setup, ensuring proper handling of Python installation scenarios and environment checks.
This commit is contained in:
DEV_DSW
2026-04-24 17:02:59 +08:00
parent e11a2296cc
commit 4c61e93c3e
42 changed files with 12560 additions and 224 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { setLocale } from '../src/i18n';
import ChatMessageList from '../src/components/chat/ChatMessageList';
@@ -17,7 +18,7 @@ vi.mock('../src/lib/skills-api', () => ({
describe('ChatMessageList', () => {
beforeEach(() => {
setLocale('zh');
setLocale('en');
vi.clearAllMocks();
mocks.apiOpenSkillPath.mockResolvedValue(undefined);
mocks.apiOpenSkillReadme.mockResolvedValue(undefined);
@@ -37,9 +38,9 @@ describe('ChatMessageList', () => {
{
id: 'user-1',
role: 'user',
name: '',
name: 'Me',
time: '10:00',
content: '帮我安装这个 skill',
content: 'Install this skill for me.',
},
]}
streamingTools={[
@@ -55,9 +56,9 @@ describe('ChatMessageList', () => {
/>,
);
expect(screen.getByText('正在执行工具...')).toBeTruthy();
expect(screen.getByText('安装 Skill')).toBeTruthy();
expect(screen.getByText('运行中')).toBeTruthy();
expect(screen.getByText('Running tools...')).toBeTruthy();
expect(screen.getByText('Install Skill')).toBeTruthy();
expect(screen.getByText('Running')).toBeTruthy();
expect(screen.getByText('Installing minimax-xlsx')).toBeTruthy();
});
@@ -70,7 +71,7 @@ describe('ChatMessageList', () => {
role: 'assistant',
name: 'YINIAN',
time: '10:01',
content: '已安装完成',
content: 'The page is open.',
isStreaming: true,
},
]}
@@ -81,18 +82,18 @@ describe('ChatMessageList', () => {
name: 'browser.open_url',
status: 'completed',
durationMs: 1200,
summary: '已为你打开 http://www.baidu.com/',
summary: 'Opened http://www.baidu.com/',
updatedAt: Date.now(),
},
]}
/>,
);
expect(screen.getByText('打开网页')).toBeTruthy();
expect(screen.getByText('已完成')).toBeTruthy();
expect(screen.getByText('Open Webpage')).toBeTruthy();
expect(screen.getByText('Completed')).toBeTruthy();
expect(screen.getByText('1.2s')).toBeTruthy();
expect(screen.getByText('已为你打开 http://www.baidu.com/')).toBeTruthy();
expect(screen.getByText('已安装完成')).toBeTruthy();
expect(screen.getByText('Opened http://www.baidu.com/')).toBeTruthy();
expect(screen.getByText('The page is open.')).toBeTruthy();
});
it('renders persistent skill-install tool cards with follow-up actions', async () => {
@@ -104,14 +105,14 @@ describe('ChatMessageList', () => {
role: 'assistant',
name: 'YINIAN',
time: '10:03',
content: '已安装并启用 skill minimax-xlsxgithub-url。位置/tmp/minimax-xlsx',
content: 'Installed and enabled minimax-xlsx at /tmp/minimax-xlsx',
toolStatuses: [
{
id: 'tool-3',
toolCallId: 'skills.install:run-3',
name: 'skills.install',
status: 'completed',
summary: '已安装并启用 skill minimax-xlsxgithub-url。位置/tmp/minimax-xlsx',
summary: 'Installed and enabled minimax-xlsx at /tmp/minimax-xlsx',
durationMs: 980,
updatedAt: Date.now(),
input: {
@@ -130,12 +131,12 @@ describe('ChatMessageList', () => {
/>,
);
expect(screen.getAllByText('已安装并启用 skill minimax-xlsxgithub-url。位置/tmp/minimax-xlsx')).toHaveLength(1);
expect(screen.getAllByText('Installed and enabled minimax-xlsx at /tmp/minimax-xlsx')).toHaveLength(1);
expect(screen.getByText('Skill')).toBeTruthy();
expect(screen.getByText('/tmp/minimax-xlsx')).toBeTruthy();
expect(screen.getByText('后续动作')).toBeTruthy();
expect(screen.getByText('Next actions')).toBeTruthy();
fireEvent.click(screen.getByText('打开目录'));
fireEvent.click(screen.getByText('Open folder'));
await waitFor(() => {
expect(mocks.apiOpenSkillPath).toHaveBeenCalledWith(
'minimax-xlsx',
@@ -144,10 +145,291 @@ describe('ChatMessageList', () => {
);
});
fireEvent.click(screen.getByText('复制路径'));
fireEvent.click(screen.getByText('Copy path'));
await waitFor(() => {
expect(mocks.writeText).toHaveBeenCalledWith('/tmp/minimax-xlsx');
});
expect(screen.getByText('路径已复制')).toBeTruthy();
expect(screen.getByText('Path copied')).toBeTruthy();
});
it('renders spreadsheet analysis cards from structured tool results', () => {
render(
<ChatMessageList
messages={[
{
id: 'assistant-sheet',
role: 'assistant',
name: 'YINIAN',
time: '10:05',
content: '',
toolStatuses: [
{
id: 'tool-4',
toolCallId: 'minimax-xlsx:run-4',
name: 'minimax-xlsx',
status: 'completed',
summary: 'Analyzed 1 spreadsheet attachment.',
updatedAt: Date.now(),
result: {
ok: true,
summary: 'Analyzed 1 spreadsheet attachment.',
renderHints: {
card: 'document-analysis',
preferredView: 'table',
skillType: 'spreadsheet',
},
artifacts: [
{
kind: 'file',
name: 'hotel-sales.xls',
filePath: 'F:\\Downloads\\hotel-sales.xls',
},
],
structuredData: {
reports: [
{
fileName: 'hotel-sales.xls',
filePath: 'F:\\Downloads\\hotel-sales.xls',
report: {
engine: 'node-xlsx',
workbook: {
sheetNames: ['Grid Results'],
totalRows: 18358,
},
structure: {
'Grid Results': {
shape: {
rows: 18358,
cols: 12,
},
columns: ['channel', 'sales_amount', 'sales_date', 'room_type'],
preview: [
{
channel: 'Douyin',
sales_amount: 615.53,
sales_date: '2025-08-10',
room_type: 'Superior Twin',
},
{
channel: 'Ctrip',
sales_amount: 983.15,
sales_date: '2025-08-11',
room_type: 'Parent Child',
},
],
},
},
quality: {
'Grid Results': [
{
type: 'missing_values',
message: 'sales_date missing in 2,566 rows',
},
],
},
},
},
],
},
},
},
],
},
]}
/>,
);
const spreadsheetCard = screen.getByTestId('tool-spreadsheet-preview');
expect(within(spreadsheetCard).getByText('Analysis overview')).toBeTruthy();
expect(within(spreadsheetCard).getByText('1 file')).toBeTruthy();
expect(within(spreadsheetCard).getAllByText('1 sheet').length).toBeGreaterThan(0);
expect(within(spreadsheetCard).getAllByText('18,358 rows').length).toBeGreaterThan(0);
expect(within(spreadsheetCard).getByText('hotel-sales.xls')).toBeTruthy();
expect(within(spreadsheetCard).getByText('Grid Results')).toBeTruthy();
expect(within(spreadsheetCard).getByText('12 cols')).toBeTruthy();
expect(within(spreadsheetCard).getByText(/Columns: channel, sales_amount, sales_date, room_type/)).toBeTruthy();
expect(within(spreadsheetCard).getByText('Douyin')).toBeTruthy();
expect(within(spreadsheetCard).getByText('615.53')).toBeTruthy();
expect(within(spreadsheetCard).getByText('sales_date missing in 2,566 rows')).toBeTruthy();
expect(screen.getByText('Artifacts')).toBeTruthy();
expect(screen.getByText('F:\\Downloads\\hotel-sales.xls')).toBeTruthy();
});
it('renders generic document-analysis structured data when there is no spreadsheet preview', () => {
render(
<ChatMessageList
messages={[
{
id: 'assistant-doc',
role: 'assistant',
name: 'YINIAN',
time: '10:06',
content: '',
toolStatuses: [
{
id: 'tool-5',
toolCallId: 'doc-skill:run-5',
name: 'contract-analyzer',
status: 'completed',
summary: 'Reviewed the contract package.',
updatedAt: Date.now(),
result: {
ok: true,
renderHints: {
card: 'document-analysis',
preferredView: 'table',
skillType: 'document',
},
structuredData: {
confidence: 0.94,
documents: ['master-service-agreement.pdf'],
findings: [
{
section: 'Termination',
status: 'Needs review',
},
{
section: 'Payment',
status: 'OK',
},
],
},
},
},
],
},
]}
/>,
);
const structuredCard = screen.getByTestId('tool-structured-result');
expect(within(structuredCard).getByText('Structured result')).toBeTruthy();
expect(within(structuredCard).getByText('Confidence')).toBeTruthy();
expect(within(structuredCard).getByText('0.94')).toBeTruthy();
expect(within(structuredCard).getByText('Documents')).toBeTruthy();
expect(within(structuredCard).getByText('master-service-agreement.pdf')).toBeTruthy();
expect(within(structuredCard).getByText('Termination')).toBeTruthy();
expect(within(structuredCard).getByText('Needs review')).toBeTruthy();
});
it('renders dedicated search result cards for search-family tool results', () => {
render(
<ChatMessageList
messages={[
{
id: 'assistant-search',
role: 'assistant',
name: 'YINIAN',
time: '10:08',
content: '',
toolStatuses: [
{
id: 'tool-6',
toolCallId: 'brave-web-search:run-6',
name: 'brave-web-search',
status: 'completed',
summary: 'Search completed with Brave Web Search.',
updatedAt: Date.now(),
result: {
ok: true,
renderHints: {
card: 'search-results',
preferredView: 'summary',
skillType: 'search',
},
structuredData: {
provider: 'brave',
query: 'hotel revenue trends',
answer: 'Revenue continues to improve across the sector.',
responseTimeMs: 820,
resultCount: 2,
results: [
{
title: 'Hotel Revenue Trends Q1',
url: 'https://example.com/revenue-q1',
snippet: 'Revenue increased across the segment.',
source: 'Example News',
score: 0.9821,
},
{
title: 'Demand Outlook',
url: 'https://example.com/demand',
snippet: 'Demand remains resilient.',
source: 'Industry Daily',
},
],
},
},
},
],
},
]}
/>,
);
const searchCard = screen.getByTestId('tool-search-results');
expect(within(searchCard).getByText('Search overview')).toBeTruthy();
expect(within(searchCard).getByText('Provider: brave')).toBeTruthy();
expect(within(searchCard).getByText('2 results')).toBeTruthy();
expect(within(searchCard).getByText('Query')).toBeTruthy();
expect(within(searchCard).getByText('hotel revenue trends')).toBeTruthy();
expect(within(searchCard).getByText('Answer')).toBeTruthy();
expect(within(searchCard).getByText('Revenue continues to improve across the sector.')).toBeTruthy();
expect(within(searchCard).getByText('Hotel Revenue Trends Q1')).toBeTruthy();
expect(within(searchCard).getByText('https://example.com/revenue-q1')).toBeTruthy();
expect(within(searchCard).getByText('Source: Example News')).toBeTruthy();
expect(within(searchCard).getByText('Score: 0.9821')).toBeTruthy();
});
it('renders install commands in the search result card for command-style tool results', () => {
render(
<ChatMessageList
messages={[
{
id: 'assistant-find-skills',
role: 'assistant',
name: 'YINIAN',
time: '10:09',
content: '',
toolStatuses: [
{
id: 'tool-7',
toolCallId: 'find-skills:run-7',
name: 'find-skills',
status: 'completed',
summary: 'Found 1 matching skill result.',
updatedAt: Date.now(),
result: {
ok: true,
renderHints: {
card: 'search-results',
preferredView: 'summary',
skillType: 'command',
},
structuredData: {
provider: 'clawhub',
query: 'react performance',
resultCount: 1,
results: [
{
title: 'react-best-practices',
url: 'https://skills.sh/vercel-labs/agent-skills/react-best-practices',
snippet: 'React and Next.js guidance from Vercel.',
installCommand: 'npx skills add vercel-labs/agent-skills@react-best-practices -g -y',
},
],
},
},
},
],
},
]}
/>,
);
const searchCard = screen.getByTestId('tool-search-results');
expect(within(searchCard).getByText('react-best-practices')).toBeTruthy();
expect(within(searchCard).getByText('Install command')).toBeTruthy();
expect(within(searchCard).getByText('npx skills add vercel-labs/agent-skills@react-best-practices -g -y')).toBeTruthy();
});
});