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 = 'x'; const artifact = prepareHtmlArtifact(source, 'Demo', 'artifact-1'); expect(artifact.source).toBe(source); expect(artifact.sandboxedSource).toMatch(/^ { 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('')) .toThrow('cannot navigate'); expect(() => prepareHtmlArtifact('')) .toThrow('navigation, or refresh'); expect(() => prepareHtmlArtifact('')) .not.toThrow(); }); }); describe('prepareArtifactDeployment', () => { it('uses flat file pairs so Qwen tool parsing never nests source in array JSON', () => { const parameters = ARTIFACT_DEPLOY_TOOL.function.parameters as unknown as { properties: Record; 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: '
\n', 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: '
\n', }, { 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('<<>>\n' }, { 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: '' }, { 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: ' prepareArtifactDeployment([ { path: 'index.html', content: '
' }, ])).not.toThrow(); }); });