Spaces:
Running
Running
| import { renderToStaticMarkup } from 'react-dom/server'; | |
| import { describe, expect, it } from 'vitest'; | |
| import type { ModelLoadProgress, ModelOption } from '../../lib/contracts'; | |
| import { ChatSurface } from './ChatSurface'; | |
| const model: ModelOption = { | |
| id: 'bonsai-1.7b', | |
| manifestId: '1_7b', | |
| label: 'Bonsai 1.7B', | |
| architectureLabel: 'Qwen3 dense', | |
| parameters: '1.7B', | |
| runtimeLabel: 'WebGPU · WASM fallback', | |
| footprint: '237 MiB', | |
| availability: 'loaded', | |
| contextLimit: 32_768, | |
| defaultContext: 4_096, | |
| }; | |
| const progress: ModelLoadProgress = { | |
| modelId: model.id, | |
| modelLabel: model.label, | |
| stage: 'ready', | |
| stageProgress: 1, | |
| downloadedBytes: 1, | |
| totalBytes: 1, | |
| residentBytes: 1, | |
| nativeStage: null, | |
| shards: [], | |
| }; | |
| describe('ChatSurface message actions', () => { | |
| it('puts copy and role-specific regeneration controls under messages', () => { | |
| const html = renderToStaticMarkup( | |
| <ChatSurface | |
| activeModel={model} | |
| loadedModelId={model.id} | |
| messages={[ | |
| { id: 'user', role: 'user', content: 'Write code', timestamp: '10:00' }, | |
| { id: 'assistant', role: 'assistant', content: 'Done', timestamp: '10:01' }, | |
| ]} | |
| streamState="complete" | |
| modelLoadProgress={progress} | |
| toolsEnabled={false} | |
| runtimeStatus="Ready" | |
| error={null} | |
| shardRetry={null} | |
| theme="light" | |
| onLoadModel={() => undefined} | |
| onDismissError={() => undefined} | |
| onRetryShard={() => undefined} | |
| onSend={() => undefined} | |
| onRegenerate={() => undefined} | |
| onEditUserMessage={() => undefined} | |
| onCancel={() => undefined} | |
| onToolsEnabledChange={() => undefined} | |
| onOpenConversationList={() => undefined} | |
| onOpenSettings={() => undefined} | |
| />, | |
| ); | |
| expect(html.match(/aria-label="Copy message"/g)).toHaveLength(2); | |
| expect(html).toContain('aria-label="Regenerate from this message"'); | |
| expect(html).toContain('aria-label="Regenerate this response"'); | |
| expect(html).toContain('aria-label="Edit message and regenerate response"'); | |
| expect(html).not.toContain('Regenerate last'); | |
| expect(html).toContain('aria-label="Show available tools"'); | |
| expect(html).toContain('<code>html_artifact</code>'); | |
| expect(html).toContain('<code>artifact_update</code>'); | |
| expect(html).toContain('<code>bar_chart</code>'); | |
| expect(html).toContain('<code>pie_chart</code>'); | |
| expect(html).toContain('<code>web_search</code>'); | |
| expect(html).toContain('Network tools ask before sending a query.'); | |
| }); | |
| it('keeps text visible and identifies a structured tool call while streaming', () => { | |
| const html = renderToStaticMarkup( | |
| <ChatSurface | |
| activeModel={model} | |
| loadedModelId={model.id} | |
| messages={[ | |
| { | |
| id: 'text', | |
| role: 'assistant', | |
| content: 'Visible streamed text', | |
| timestamp: '10:00', | |
| state: 'streaming', | |
| }, | |
| { | |
| id: 'tool', | |
| role: 'assistant', | |
| content: '', | |
| timestamp: '10:01', | |
| state: 'streaming', | |
| toolActivity: { name: 'html_artifact', argumentCharacters: 12_345 }, | |
| }, | |
| ]} | |
| streamState="streaming" | |
| modelLoadProgress={progress} | |
| toolsEnabled | |
| runtimeStatus="Generating" | |
| error={null} | |
| shardRetry={null} | |
| theme="light" | |
| onLoadModel={() => undefined} | |
| onDismissError={() => undefined} | |
| onRetryShard={() => undefined} | |
| onSend={() => undefined} | |
| onRegenerate={() => undefined} | |
| onEditUserMessage={() => undefined} | |
| onCancel={() => undefined} | |
| onToolsEnabledChange={() => undefined} | |
| onOpenConversationList={() => undefined} | |
| onOpenSettings={() => undefined} | |
| />, | |
| ); | |
| expect(html).toContain('Visible streamed text'); | |
| expect(html).toContain('Generating html_artifact tool call'); | |
| expect(html).toContain('<code>html_artifact</code>'); | |
| expect(html).toContain('12,345 chars'); | |
| expect(html.match(/stream-cursor/g)).toHaveLength(2); | |
| }); | |
| it('renders generated artifacts inline with HTML and Rendered tabs', () => { | |
| const html = renderToStaticMarkup( | |
| <ChatSurface | |
| activeModel={model} | |
| loadedModelId={model.id} | |
| messages={[{ | |
| id: 'assistant', | |
| role: 'assistant', | |
| content: 'Here is the app.', | |
| timestamp: '10:01', | |
| artifacts: [{ | |
| id: 'artifact-one', | |
| title: 'Counter app', | |
| source: '<main>Counter</main>', | |
| sandboxedSource: '<main>Counter</main>', | |
| runtimeToken: 'artifact-one-runtime', | |
| createdAt: 1, | |
| }], | |
| }]} | |
| streamState="complete" | |
| modelLoadProgress={progress} | |
| toolsEnabled | |
| runtimeStatus="Ready" | |
| error={null} | |
| shardRetry={null} | |
| theme="light" | |
| onLoadModel={() => undefined} | |
| onDismissError={() => undefined} | |
| onRetryShard={() => undefined} | |
| onSend={() => undefined} | |
| onRegenerate={() => undefined} | |
| onEditUserMessage={() => undefined} | |
| onCancel={() => undefined} | |
| onToolsEnabledChange={() => undefined} | |
| onOpenConversationList={() => undefined} | |
| onOpenSettings={() => undefined} | |
| />, | |
| ); | |
| expect(html).toContain('id="artifact-artifact-one"'); | |
| expect(html).toContain('role="tab" aria-selected="true"'); | |
| expect(html).toContain('>HTML</button>'); | |
| expect(html).toContain('>Rendered</button>'); | |
| expect(html).toContain('<main>Counter</main>'); | |
| expect(html).not.toContain('<iframe'); | |
| }); | |
| it('keeps text, tool results, artifacts, and later text in chronological order', () => { | |
| const html = renderToStaticMarkup( | |
| <ChatSurface | |
| activeModel={model} | |
| loadedModelId={model.id} | |
| messages={[{ | |
| id: 'assistant', | |
| role: 'assistant', | |
| content: 'Before the chart. After the chart.', | |
| timestamp: '10:01', | |
| segments: [ | |
| { id: 'before', kind: 'text', content: 'Before the chart.' }, | |
| { id: 'chart-call', kind: 'tool', toolCallId: 'tool-one' }, | |
| { id: 'after', kind: 'text', content: 'After the chart.' }, | |
| ], | |
| tools: [{ | |
| id: 'tool-one', | |
| name: 'bar_chart', | |
| input: '{"title":"Quarterly sales"}', | |
| output: '{"ok":true,"evaluation":{"status":"passed","errors":[]}}', | |
| state: 'complete', | |
| artifactId: 'artifact-one', | |
| }], | |
| artifacts: [{ | |
| id: 'artifact-one', | |
| title: 'Quarterly sales', | |
| source: '<main>Quarterly sales</main>', | |
| sandboxedSource: '<main>Quarterly sales</main>', | |
| runtimeToken: 'artifact-one-runtime', | |
| createdAt: 1, | |
| }], | |
| }]} | |
| streamState="complete" | |
| modelLoadProgress={progress} | |
| toolsEnabled | |
| runtimeStatus="Ready" | |
| error={null} | |
| shardRetry={null} | |
| theme="light" | |
| onLoadModel={() => undefined} | |
| onDismissError={() => undefined} | |
| onRetryShard={() => undefined} | |
| onSend={() => undefined} | |
| onRegenerate={() => undefined} | |
| onEditUserMessage={() => undefined} | |
| onCancel={() => undefined} | |
| onToolsEnabledChange={() => undefined} | |
| onOpenConversationList={() => undefined} | |
| onOpenSettings={() => undefined} | |
| />, | |
| ); | |
| const before = html.indexOf('Before the chart.'); | |
| const tool = html.indexOf('<code>bar_chart</code>'); | |
| const artifact = html.indexOf('id="artifact-artifact-one"'); | |
| const after = html.indexOf('After the chart.'); | |
| expect(before).toBeGreaterThan(-1); | |
| expect(tool).toBeGreaterThan(before); | |
| expect(artifact).toBeGreaterThan(tool); | |
| expect(after).toBeGreaterThan(artifact); | |
| }); | |
| }); | |