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:
@@ -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-xlsx(github-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-xlsx(github-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-xlsx(github-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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user