Bonsai-Chat-WebGPU / src /app /App.test.ts
WaveCut's picture
Persist chat tools and stream artifact drafts
179a355 verified
Raw
History Blame Contribute Delete
12.7 kB
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.' },
]);
});
});