Spaces:
Paused
Paused
| import { describe, expect, it } from 'vitest'; | |
| import releaseManifest from '../../public/manifest/models.json'; | |
| import { EngineClientError } from '../engine'; | |
| import { | |
| applyToolExecutionToMessages, | |
| browserTabTitle, | |
| buildEngineHistory, | |
| DEFAULT_CONTEXT_SIZE, | |
| DEFAULT_MAX_TOKENS, | |
| DEFAULT_MODEL_ID, | |
| initialInferencePhase, | |
| liveInferencePhase, | |
| messagesForPersistence, | |
| modelArchitectureLabel, | |
| normalizeStoredMessages, | |
| preparingInferenceStatus, | |
| publishBackendReport, | |
| reconcileCompletionTokens, | |
| reconcileTotalTokens, | |
| reportsModelWeightProgress, | |
| requiresModelReload, | |
| shouldCachePrompt, | |
| } from './App'; | |
| import { prepareArtifactDeployment, prepareHtmlArtifact, type ToolExecution } from '../lib/agent-tools'; | |
| function engineError(code: string): EngineClientError { | |
| return new EngineClientError({ code, message: code }); | |
| } | |
| describe('App model invalidation policy', () => { | |
| it.each([ | |
| 'WEBGPU_DEVICE_LOST', | |
| 'ENGINE_WORKER_FAILED', | |
| 'MODEL_NOT_LOADED', | |
| 'MODEL_RESOURCE_EXHAUSTED', | |
| ])('requires an explicit reload after %s', (code) => { | |
| expect(requiresModelReload(engineError(code))).toBe(true); | |
| }); | |
| it('keeps an otherwise healthy loaded model after a responsive abort', () => { | |
| expect(requiresModelReload(engineError('ABORTED'))).toBe(false); | |
| }); | |
| }); | |
| describe('App release defaults', () => { | |
| it('opens on the flagship tier required by the product contract', () => { | |
| expect(DEFAULT_MODEL_ID).toBe('bonsai-27b'); | |
| }); | |
| it('shows manifest architecture instead of invented model aliases', () => { | |
| expect(modelArchitectureLabel('qwen3')).toBe('Qwen3 dense'); | |
| expect(modelArchitectureLabel('qwen35')).toBe('Qwen3.5 hybrid'); | |
| }); | |
| it('defaults both context and maximum completion to 8K', () => { | |
| expect(DEFAULT_CONTEXT_SIZE).toBe(8192); | |
| expect(DEFAULT_MAX_TOKENS).toBe(8192); | |
| expect(releaseManifest.models.every((model) => model.defaultContext === DEFAULT_CONTEXT_SIZE)) | |
| .toBe(true); | |
| }); | |
| it('keeps prompt reuse for ordinary chat but avoids recurrent-state drift in Qwen3.5 tool runs', () => { | |
| expect(shouldCachePrompt('qwen35', false)).toBe(true); | |
| expect(shouldCachePrompt('qwen35', true)).toBe(false); | |
| expect(shouldCachePrompt('qwen3', true)).toBe(true); | |
| }); | |
| it('advertises resident model state in the browser tab title', () => { | |
| expect(browserTabTitle(null, null, 'idle')).toBe('⚪ Model not loaded · Bonsai Chat'); | |
| expect(browserTabTitle(null, 'Bonsai 27B', 'loading')).toBe('⏳ Loading Bonsai 27B · Bonsai Chat'); | |
| expect(browserTabTitle('Bonsai 27B', null, 'complete')).toBe('🟢 Bonsai 27B loaded · Bonsai Chat'); | |
| expect(browserTabTitle('Bonsai 27B', null, 'streaming')).toBe('✨ Bonsai 27B generating · Bonsai Chat'); | |
| }); | |
| it('only exposes preparation before the first graph has produced progress', () => { | |
| expect(initialInferencePhase(false)).toBe('preparing'); | |
| expect(initialInferencePhase(true)).toBe('idle'); | |
| expect(liveInferencePhase('preparing', false)).toBe('preparing'); | |
| expect(liveInferencePhase('preparing', true)).toBe('prefill'); | |
| expect(preparingInferenceStatus(3.25)) | |
| .toBe('Preparing WebGPU · compiling first inference graph · 3.3 s'); | |
| }); | |
| }); | |
| describe('App model-load progress', () => { | |
| it('does not turn pinned manifest metadata reads into model-weight download state', () => { | |
| expect(reportsModelWeightProgress('manifest')).toBe(false); | |
| expect(reportsModelWeightProgress('download')).toBe(true); | |
| expect(reportsModelWeightProgress('load')).toBe(true); | |
| expect(reportsModelWeightProgress('warmup')).toBe(true); | |
| }); | |
| }); | |
| describe('App streamed completion telemetry', () => { | |
| it('uses observed stream events when final engine usage is incomplete', () => { | |
| expect(reconcileCompletionTokens(5, 64)).toBe(64); | |
| }); | |
| it('keeps the engine count when one stream event contains multiple tokens', () => { | |
| expect(reconcileCompletionTokens(12, 8)).toBe(12); | |
| }); | |
| it('fails closed to the observed count for invalid engine usage', () => { | |
| expect(reconcileCompletionTokens(Number.NaN, 7)).toBe(7); | |
| }); | |
| it('keeps context usage consistent with a reconciled completion count', () => { | |
| expect(reconcileTotalTokens(5, 5, 64)).toBe(69); | |
| expect(reconcileTotalTokens(80, 5, 64)).toBe(80); | |
| }); | |
| }); | |
| describe('App conversation persistence', () => { | |
| it('does not persist transient streamed artifact drafts', () => { | |
| expect(messagesForPersistence([{ | |
| id: 'assistant', | |
| role: 'assistant', | |
| content: '', | |
| timestamp: '10:00', | |
| toolActivity: { | |
| name: 'artifact_deploy', | |
| argumentCharacters: 120, | |
| artifactDraft: { | |
| id: 'draft', | |
| title: 'Draft', | |
| entryPath: 'index.html', | |
| files: [], | |
| updatedAt: 1, | |
| }, | |
| }, | |
| }])).toEqual([{ | |
| id: 'assistant', | |
| role: 'assistant', | |
| content: '', | |
| timestamp: '10:00', | |
| }]); | |
| }); | |
| }); | |
| describe('App backend diagnostics', () => { | |
| it('publishes the complete backend report for the browser gate without rendering it', () => { | |
| const report = { | |
| backends: ['WebGPU'], | |
| nGraphSplits: 1, | |
| opsOnCpu: 0, | |
| layersGpu: { offloaded: 65, total: 65 }, | |
| flashAttention: false, | |
| cacheTypeK: 'f16', | |
| cacheTypeV: 'f16', | |
| webgpuKvBufferBytes: 1024, | |
| webgpuTrace: ['@@WEBGPU_TRACE@@completion_end id=1 steps=65 error=0'], | |
| }; | |
| publishBackendReport(report); | |
| expect(globalThis.__bonsaiBackendReport).toEqual(report); | |
| delete globalThis.__bonsaiBackendReport; | |
| }); | |
| }); | |
| describe('App artifact identity', () => { | |
| it('moves an edited artifact to the current tool position while completing the tool run', () => { | |
| const original = prepareArtifactDeployment([ | |
| { path: 'index.html', content: '<script type="module" src="./src/main.js"></script>' }, | |
| { path: 'src/main.js', content: 'document.body.textContent = "old";' }, | |
| ], 'Demo', 'index.html', 'stable-artifact', 'old-runtime'); | |
| const replacement = prepareArtifactDeployment([ | |
| { path: 'index.html', content: '<script type="module" src="./src/main.js"></script>' }, | |
| { path: 'src/main.js', content: 'document.body.textContent = "new";' }, | |
| ], 'Demo', 'index.html', 'stable-artifact', 'new-runtime'); | |
| const execution: ToolExecution = { | |
| call: { | |
| id: 'update-call', | |
| type: 'function', | |
| function: { name: 'artifact_write', arguments: '{}' }, | |
| }, | |
| artifact: replacement, | |
| relocateArtifact: true, | |
| output: '{"ok":true}', | |
| failed: false, | |
| }; | |
| const next = applyToolExecutionToMessages([ | |
| { id: 'older', role: 'assistant', content: '', timestamp: '10:00', artifacts: [original] }, | |
| { | |
| id: 'current', | |
| role: 'assistant', | |
| content: '', | |
| timestamp: '10:01', | |
| tools: [{ id: 'update-call', name: 'artifact_write', input: '{}', state: 'running' }], | |
| }, | |
| ], 'current', 'update-call', execution); | |
| expect(next[0]?.artifacts).toEqual([]); | |
| expect(next[1]?.artifacts).toEqual([replacement]); | |
| expect(next[1]?.tools?.[0]).toMatchObject({ | |
| state: 'complete', | |
| output: '{"ok":true}', | |
| artifactId: 'stable-artifact', | |
| }); | |
| }); | |
| it('keeps a test-only artifact in place while returning diagnostics to the current turn', () => { | |
| const artifact = prepareHtmlArtifact('<p>tested</p>', 'Demo', 'stable-artifact', 'runtime'); | |
| const execution: ToolExecution = { | |
| call: { id: 'test-call', type: 'function', function: { name: 'artifact_test', arguments: '{}' } }, | |
| artifact, | |
| relocateArtifact: false, | |
| output: '{"ok":true}', | |
| failed: false, | |
| }; | |
| const next = applyToolExecutionToMessages([ | |
| { id: 'older', role: 'assistant', content: '', timestamp: '10:00', artifacts: [artifact] }, | |
| { | |
| id: 'current', | |
| role: 'assistant', | |
| content: '', | |
| timestamp: '10:01', | |
| tools: [{ id: 'test-call', name: 'artifact_test', input: '{}', state: 'running' }], | |
| }, | |
| ], 'current', 'test-call', execution); | |
| expect(next[0]?.artifacts).toEqual([artifact]); | |
| expect(next[1]?.artifacts).toBeUndefined(); | |
| expect(next[1]?.tools?.[0]).toMatchObject({ state: 'complete', artifactId: 'stable-artifact' }); | |
| }); | |
| it('attaches a newly created artifact to the current assistant message', () => { | |
| const artifact = prepareArtifactDeployment([ | |
| { path: 'index.html', content: '<link rel="stylesheet" href="./styles/app.css"><p>new</p>' }, | |
| { path: 'styles/app.css', content: 'p { color: green; }' }, | |
| ], 'Demo', 'index.html', 'new-artifact', 'runtime'); | |
| const execution: ToolExecution = { | |
| call: { id: 'create-call', type: 'function', function: { name: 'artifact_deploy', arguments: '{}' } }, | |
| artifact, | |
| relocateArtifact: true, | |
| output: '{"ok":true}', | |
| failed: false, | |
| }; | |
| const next = applyToolExecutionToMessages([{ | |
| id: 'current', | |
| role: 'assistant', | |
| content: '', | |
| timestamp: '10:01', | |
| tools: [{ id: 'create-call', name: 'artifact_deploy', input: '{}', state: 'running' }], | |
| }], 'current', 'create-call', execution); | |
| expect(next[0]?.artifacts).toEqual([artifact]); | |
| expect(next[0]?.tools?.[0]?.artifactId).toBe('new-artifact'); | |
| }); | |
| }); | |
| describe('App persisted agent history', () => { | |
| it('keeps a conversation readable when an old artifact contains truncated HTML', () => { | |
| const [message] = normalizeStoredMessages([{ | |
| id: 'assistant', | |
| role: 'assistant', | |
| content: 'The deployment attempt was interrupted.', | |
| timestamp: '10:00', | |
| artifacts: [{ | |
| id: 'truncated', | |
| title: 'Snake Game', | |
| source: '<!DOCTYPE html><html lang=', | |
| sandboxedSource: '<!DOCTYPE html><html lang=', | |
| runtimeToken: 'stored-runtime', | |
| createdAt: 1, | |
| entryPath: 'index.html', | |
| files: [{ | |
| path: 'index.html', | |
| content: '<!DOCTYPE html><html lang=', | |
| mime: 'text/html; charset=utf-8', | |
| bytes: 26, | |
| }], | |
| }], | |
| }]); | |
| expect(message?.content).toBe('The deployment attempt was interrupted.'); | |
| expect(message?.artifacts?.[0]).toMatchObject({ | |
| id: 'truncated', | |
| source: '<!DOCTYPE html><html lang=', | |
| evaluation: { | |
| status: 'failed', | |
| entries: [{ | |
| level: 'error', | |
| message: expect.stringContaining('unterminated HTML tag'), | |
| }], | |
| }, | |
| }); | |
| }); | |
| it('reconstructs separate assistant and tool rounds from ordered segments', () => { | |
| expect(buildEngineHistory([{ | |
| id: 'assistant', | |
| role: 'assistant', | |
| content: 'I will calculate. The first result is ready. Final answer.', | |
| timestamp: '10:00', | |
| segments: [ | |
| { id: 'text-0', kind: 'text', content: 'I will calculate.' }, | |
| { id: 'tool-1', kind: 'tool', toolCallId: 'call-1', roundId: 'round-0' }, | |
| { id: 'reasoning-1', kind: 'reasoning', content: 'Checking the first result.' }, | |
| { id: 'text-1', kind: 'text', content: 'The first result is ready.' }, | |
| { id: 'tool-2', kind: 'tool', toolCallId: 'call-2', roundId: 'round-1' }, | |
| { id: 'tool-3', kind: 'tool', toolCallId: 'call-3', roundId: 'round-1' }, | |
| { id: 'reasoning-2', kind: 'reasoning', content: 'Combining results.' }, | |
| { id: 'text-2', kind: 'text', content: 'Final answer.' }, | |
| ], | |
| tools: [ | |
| { id: 'call-1', name: 'js_eval', input: '{"code":"1+1"}', output: '{"value":2}', state: 'complete' }, | |
| { id: 'call-2', name: 'js_eval', input: '{"code":"2+2"}', output: '{"value":4}', state: 'complete' }, | |
| { id: 'call-3', name: 'js_eval', input: '{"code":"3+3"}', output: '{"value":6}', state: 'complete' }, | |
| ], | |
| }], 'System')).toEqual([ | |
| { role: 'system', content: 'System' }, | |
| { | |
| role: 'assistant', | |
| content: 'I will calculate.', | |
| tool_calls: [{ | |
| id: 'call-1', | |
| type: 'function', | |
| function: { name: 'js_eval', arguments: '{"code":"1+1"}' }, | |
| }], | |
| }, | |
| { role: 'tool', content: '{"value":2}', tool_call_id: 'call-1' }, | |
| { | |
| role: 'assistant', | |
| content: 'The first result is ready.', | |
| tool_calls: [ | |
| { id: 'call-2', type: 'function', function: { name: 'js_eval', arguments: '{"code":"2+2"}' } }, | |
| { id: 'call-3', type: 'function', function: { name: 'js_eval', arguments: '{"code":"3+3"}' } }, | |
| ], | |
| }, | |
| { role: 'tool', content: '{"value":4}', tool_call_id: 'call-2' }, | |
| { role: 'tool', content: '{"value":6}', tool_call_id: 'call-3' }, | |
| { role: 'assistant', content: 'Final answer.' }, | |
| ]); | |
| }); | |
| }); | |