Spaces:
Running
Running
| 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(); | |
| }); | |
| }); | |