- 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.
436 lines
16 KiB
TypeScript
436 lines
16 KiB
TypeScript
import React from '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';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
apiOpenSkillPath: vi.fn(),
|
|
apiOpenSkillReadme: vi.fn(),
|
|
writeText: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../src/lib/skills-api', () => ({
|
|
apiOpenSkillPath: mocks.apiOpenSkillPath,
|
|
apiOpenSkillReadme: mocks.apiOpenSkillReadme,
|
|
}));
|
|
|
|
describe('ChatMessageList', () => {
|
|
beforeEach(() => {
|
|
setLocale('en');
|
|
vi.clearAllMocks();
|
|
mocks.apiOpenSkillPath.mockResolvedValue(undefined);
|
|
mocks.apiOpenSkillReadme.mockResolvedValue(undefined);
|
|
mocks.writeText.mockResolvedValue(undefined);
|
|
Object.defineProperty(window.navigator, 'clipboard', {
|
|
value: {
|
|
writeText: mocks.writeText,
|
|
},
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
it('renders standalone streaming tool status cards when there is no streaming assistant text yet', () => {
|
|
render(
|
|
<ChatMessageList
|
|
messages={[
|
|
{
|
|
id: 'user-1',
|
|
role: 'user',
|
|
name: 'Me',
|
|
time: '10:00',
|
|
content: 'Install this skill for me.',
|
|
},
|
|
]}
|
|
streamingTools={[
|
|
{
|
|
id: 'tool-1',
|
|
toolCallId: 'skills.install:run-1',
|
|
name: 'skills.install',
|
|
status: 'running',
|
|
summary: 'Installing minimax-xlsx',
|
|
updatedAt: Date.now(),
|
|
},
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByText('Running tools...')).toBeTruthy();
|
|
expect(screen.getByText('Install Skill')).toBeTruthy();
|
|
expect(screen.getByText('Running')).toBeTruthy();
|
|
expect(screen.getByText('Installing minimax-xlsx')).toBeTruthy();
|
|
});
|
|
|
|
it('renders streaming tool status cards above a streaming assistant message', () => {
|
|
render(
|
|
<ChatMessageList
|
|
messages={[
|
|
{
|
|
id: 'assistant-stream',
|
|
role: 'assistant',
|
|
name: 'YINIAN',
|
|
time: '10:01',
|
|
content: 'The page is open.',
|
|
isStreaming: true,
|
|
},
|
|
]}
|
|
streamingTools={[
|
|
{
|
|
id: 'tool-2',
|
|
toolCallId: 'browser.open_url:run-2',
|
|
name: 'browser.open_url',
|
|
status: 'completed',
|
|
durationMs: 1200,
|
|
summary: 'Opened http://www.baidu.com/',
|
|
updatedAt: Date.now(),
|
|
},
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByText('Open Webpage')).toBeTruthy();
|
|
expect(screen.getByText('Completed')).toBeTruthy();
|
|
expect(screen.getByText('1.2s')).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 () => {
|
|
render(
|
|
<ChatMessageList
|
|
messages={[
|
|
{
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
name: 'YINIAN',
|
|
time: '10:03',
|
|
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: 'Installed and enabled minimax-xlsx at /tmp/minimax-xlsx',
|
|
durationMs: 980,
|
|
updatedAt: Date.now(),
|
|
input: {
|
|
kind: 'github-url',
|
|
url: 'https://github.com/MiniMax-AI/skills/blob/main/skills/minimax-xlsx/SKILL.md',
|
|
},
|
|
result: {
|
|
slug: 'minimax-xlsx',
|
|
source: 'github-url',
|
|
baseDir: '/tmp/minimax-xlsx',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
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('Next actions')).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByText('Open folder'));
|
|
await waitFor(() => {
|
|
expect(mocks.apiOpenSkillPath).toHaveBeenCalledWith(
|
|
'minimax-xlsx',
|
|
'minimax-xlsx',
|
|
'/tmp/minimax-xlsx',
|
|
);
|
|
});
|
|
|
|
fireEvent.click(screen.getByText('Copy path'));
|
|
await waitFor(() => {
|
|
expect(mocks.writeText).toHaveBeenCalledWith('/tmp/minimax-xlsx');
|
|
});
|
|
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();
|
|
});
|
|
});
|