File size: 5,599 Bytes
0ed8124
643dfc6
 
af9299f
643dfc6
 
 
 
0ed8124
 
 
 
 
 
897170b
 
0ed8124
 
897170b
 
 
0ed8124
 
 
 
 
 
 
 
897170b
 
 
 
 
33e51e5
897170b
 
 
0ed8124
33e51e5
 
af9299f
643dfc6
af9299f
 
643dfc6
af9299f
 
 
 
643dfc6
 
af9299f
 
 
 
 
 
 
643dfc6
 
 
 
 
 
 
 
 
 
 
 
 
af9299f
 
 
 
 
 
643dfc6
 
 
 
 
 
33e51e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328256a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33e51e5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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();
  });
});