Bonsai-Chat-WebGPU / src /lib /agent-tools.test.ts
WaveCut's picture
Flatten artifact file tool arguments
af9299f verified
Raw
History Blame Contribute Delete
5.6 kB
import { describe, expect, it } from 'vitest';
import {
ARTIFACT_DEPLOY_TOOL,
artifactFileInputsFromArguments,
parseArtifactFileBundle,
prepareArtifactDeployment,
prepareHtmlArtifact,
} from './agent-tools';
describe('prepareHtmlArtifact', () => {
it('injects a deny-by-default CSP while preserving source for the code view', () => {
const source = '<html><head><title>x</title></head><body><script>document.body.dataset.ok="1"</script></body></html>';
const artifact = prepareHtmlArtifact(source, 'Demo', 'artifact-1');
expect(artifact.source).toBe(source);
expect(artifact.sandboxedSource).toMatch(/^<meta http-equiv="Content-Security-Policy"/);
expect(artifact.sandboxedSource.indexOf('Content-Security-Policy')).toBeLessThan(artifact.sandboxedSource.indexOf('document.body.dataset.ok'));
expect(artifact.sandboxedSource).toContain("default-src 'none'");
expect(artifact.sandboxedSource).toContain("connect-src 'none'");
expect(artifact.sandboxedSource).toContain("script-src 'unsafe-inline'");
expect(artifact.sandboxedSource).toContain('data-bonsai-artifact-runtime');
expect(artifact.runtimeToken).toBeTruthy();
expect(artifact.title).toBe('Demo');
});
it('rejects artifacts larger than the client-side safety bound', () => {
expect(() => prepareHtmlArtifact('x'.repeat(256 * 1024 + 1), 'large', 'artifact-2')).toThrow(
'exceeds 256 KiB',
);
});
it('rejects direct document navigation while keeping ordinary inline scripts available', () => {
expect(() => prepareHtmlArtifact('<script>location.href="https://example.com"</script>'))
.toThrow('cannot navigate');
expect(() => prepareHtmlArtifact('<meta http-equiv="refresh" content="0;url=https://example.com">'))
.toThrow('navigation, or refresh');
expect(() => prepareHtmlArtifact('<script>document.body.textContent = "ok"</script>'))
.not.toThrow();
});
});
describe('prepareArtifactDeployment', () => {
it('uses flat file pairs so Qwen tool parsing never nests source in array<object> JSON', () => {
const parameters = ARTIFACT_DEPLOY_TOOL.function.parameters as unknown as {
properties: Record<string, { type: string; description: string }>;
required: string[];
};
expect(parameters.properties.files).toBeUndefined();
expect(parameters.properties.file_1_path?.type).toBe('string');
expect(parameters.properties.file_1_content?.type).toBe('string');
expect(parameters.required).toEqual(['file_1_path', 'file_1_content']);
});
it('preserves source characters from flat multi-file tool arguments', () => {
const files = artifactFileInputsFromArguments({
file_1_path: 'index.html',
file_1_content: '<!doctype html><meta charset="utf-8"><form method="post"></form>\n<script type="module" src="./app.js"></script>',
file_2_path: 'app.js',
file_2_content: 'const root = document.querySelector("#app");\nroot.dataset.value = \'quotes \\\\ stay intact\';',
}, 'artifact_deploy');
expect(files).toEqual([
{
path: 'index.html',
content: '<!doctype html><meta charset="utf-8"><form method="post"></form>\n<script type="module" src="./app.js"></script>',
},
{
path: 'app.js',
content: 'const root = document.querySelector("#app");\nroot.dataset.value = \'quotes \\\\ stay intact\';',
},
]);
});
it('rejects an incomplete flat file pair instead of executing partial source', () => {
expect(() => artifactFileInputsFromArguments({
file_1_path: 'index.html',
}, 'artifact_deploy')).toThrow('file_1_path and file_1_content together');
});
it('rejects an unterminated raw file envelope instead of executing partial source', () => {
expect(() => parseArtifactFileBundle('<<<FILE index.html>>>\n<script>')).toThrow(
'missing <<<END FILE>>>',
);
});
it('preserves a multi-file tree and rejects root-absolute resources', () => {
const artifact = prepareArtifactDeployment([
{ path: 'index.html', content: '<link rel="stylesheet" href="./styles/app.css"><script type="module" src="./src/main.js"></script>' },
{ path: 'styles/app.css', content: 'body { color: green; }' },
{ path: 'src/main.js', content: 'import "./view.js";' },
{ path: 'src/view.js', content: 'document.body.dataset.ready = "1";' },
], 'Multi-file', 'index.html', 'deployment-1', 'runtime-1');
expect(artifact.files?.map((file) => file.path)).toEqual([
'index.html',
'styles/app.css',
'src/main.js',
'src/view.js',
]);
expect(artifact.entryPath).toBe('index.html');
expect(artifact.schemaVersion).toBe(2);
expect(() => prepareArtifactDeployment([
{ path: 'index.html', content: '<script src="/src/main.js"></script>' },
{ path: 'src/main.js', content: '' },
])).toThrow('root-absolute');
});
it('rejects truncated HTML instead of accepting the browser-repaired DOM', () => {
expect(() => prepareArtifactDeployment([
{ path: 'index.html', content: '<!DOCTYPE html><html lang=' },
])).toThrow('unterminated HTML tag or quoted attribute');
expect(() => prepareArtifactDeployment([
{ path: 'index.html', content: '<main data-label="unfinished></main>' },
])).toThrow('unterminated HTML tag or quoted attribute');
});
it('allows comparison operators inside complete inline scripts', () => {
expect(() => prepareArtifactDeployment([
{ path: 'index.html', content: '<script>if (1<2) document.body.textContent = "ok";</script>' },
])).not.toThrow();
});
});