Skip to content

Commit 9126909

Browse files
test: add vitest coverage for tool render modules
Add per-renderer vitest coverage with XSS escaping assertions, a shared test helper, registry-driven smoke tests, and raised coverage thresholds.
1 parent a725bd2 commit 9126909

23 files changed

Lines changed: 758 additions & 4 deletions

static/js/render/registry.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,54 @@ describe('renderTodoWriteResult', () => {
188188
});
189189
});
190190

191+
/** Representative fixtures for registry-driven behavioral smoke tests. */
192+
const TOOL_USE_FIXTURES = {
193+
Bash: { input: { command: 'echo hi', description: 'say hi' } },
194+
Read: { input: { file_path: 'README.md' } },
195+
Write: { input: { file_path: 'out.txt', content: 'data' } },
196+
Edit: { input: { file_path: 'a.js', old_string: 'x', new_string: 'y' } },
197+
Glob: { input: { pattern: '*.js', path: 'src' } },
198+
Grep: { input: { pattern: 'TODO', path: 'lib' } },
199+
Task: { input: { subagent_type: 'explore', description: 'scan', prompt: 'go' } },
200+
TodoWrite: { input: { todos: [{ status: 'pending', content: 'task' }] } },
201+
AskUserQuestion: { input: { questions: [{ question: 'OK?' }] } },
202+
WebFetch: { input: { url: 'https://example.com' } },
203+
WebSearch: { input: { query: 'vitest' } },
204+
};
205+
206+
const TOOL_RESULT_FIXTURES = {
207+
bash: { exit_code: 0, stdout: 'ok' },
208+
file_read: { file_path: '/a.txt', num_lines: 10 },
209+
file_edit: { file_path: 'b.js' },
210+
file_write: { file_path: 'c.txt' },
211+
glob: { num_files: 3 },
212+
grep: { num_files: 2, num_lines: 5 },
213+
web_search: { query: 'test', result_count: 1 },
214+
web_fetch: { url: 'https://x.com', status_code: 200 },
215+
task: { status: 'completed', total_duration_ms: 1000 },
216+
todo_write: { todos: [{ status: 'pending', content: 'x' }] },
217+
user_input: { questions: [{ question: 'Q' }], answers: { Q: 'A' } },
218+
plan: { file_path: 'plan.md' },
219+
};
220+
221+
describe('registry behavioral smoke tests', () => {
222+
it('every registered tool_use renderer produces non-empty HTML', () => {
223+
for (const name of CORE_TOOL_USE) {
224+
const html = renderToolUse({ name, ...TOOL_USE_FIXTURES[name] });
225+
expect(html, name).toContain('tool-call');
226+
expect(html.length, name).toBeGreaterThan(20);
227+
}
228+
});
229+
230+
it('every registered tool_result renderer produces non-empty HTML', () => {
231+
for (const rt of CORE_TOOL_RESULT) {
232+
const html = renderToolResult({ result_type: rt, ...TOOL_RESULT_FIXTURES[rt] });
233+
expect(html, rt).toContain('tool-result');
234+
expect(html.length, rt).toBeGreaterThan(20);
235+
}
236+
});
237+
});
238+
191239
describe('renderToolResult fallback', () => {
192240
it('renders raw result type and JSON payload for unknown result types', () => {
193241
const html = renderToolResult({

static/js/render/test_helpers.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect } from 'vitest';
2+
import { renderToolUse, renderToolResult } from './registry.js';
3+
4+
/** Common XSS payloads for renderer escaping assertions. */
5+
export const XSS_SCRIPT = '<script>alert(1)</script>';
6+
export const XSS_IMG = '<img onerror=alert(1)>';
7+
8+
/**
9+
* Render a tool-use card via the registry and return the HTML string.
10+
*/
11+
export function mountToolUse(tool) {
12+
return renderToolUse(tool);
13+
}
14+
15+
/**
16+
* Render a tool-result card via the registry and return the HTML string.
17+
*/
18+
export function mountToolResult(parsed) {
19+
return renderToolResult(parsed);
20+
}
21+
22+
/**
23+
* Assert raw HTML fragments are escaped (not present verbatim).
24+
*/
25+
export function expectNoRawHtml(html, rawFragments) {
26+
for (const frag of rawFragments) {
27+
expect(html).not.toContain(frag);
28+
}
29+
}
30+
31+
/**
32+
* Assert an HTML-escaped fragment appears in output.
33+
*/
34+
export function expectEscaped(html, escapedFragment) {
35+
expect(html).toContain(escapedFragment);
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderBashResult } from './bash.js';
3+
import { mountToolResult, expectNoRawHtml, XSS_IMG } from '../test_helpers.js';
4+
5+
describe('renderBashResult', () => {
6+
it('renders success status with stdout', () => {
7+
const html = renderBashResult({
8+
result_type: 'bash',
9+
exit_code: 0,
10+
stdout: 'hello\nworld',
11+
});
12+
expect(html).toContain('Bash Result (success)');
13+
expect(html).toContain('hello');
14+
expect(html).toContain('stdout');
15+
expect(html).toContain('tool-result');
16+
});
17+
18+
it('renders error status with stderr', () => {
19+
const html = renderBashResult({
20+
result_type: 'bash',
21+
exit_code: 1,
22+
is_error: true,
23+
stderr: 'command failed',
24+
});
25+
expect(html).toContain('error (exit 1)');
26+
expect(html).toContain('command failed');
27+
expect(html).toContain('stderr');
28+
});
29+
30+
it('renders interrupted status', () => {
31+
const html = renderBashResult({ result_type: 'bash', interrupted: true });
32+
expect(html).toContain('Bash Result (interrupted)');
33+
});
34+
35+
it('escapes HTML in stdout and stderr', () => {
36+
const html = mountToolResult({
37+
result_type: 'bash',
38+
exit_code: 0,
39+
stdout: XSS_IMG,
40+
stderr: '<script>x</script>',
41+
});
42+
expectNoRawHtml(html, [XSS_IMG, '<script>']);
43+
expect(html).toContain('&lt;img');
44+
});
45+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderToolResultFallback } from './fallback.js';
3+
import { mountToolResult, expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';
4+
5+
describe('renderToolResultFallback', () => {
6+
it('renders unknown result type and JSON payload', () => {
7+
const html = renderToolResultFallback({
8+
result_type: 'custom_widget',
9+
widget_id: 99,
10+
label: 'test',
11+
});
12+
expect(html).toContain('Unknown tool result: custom_widget');
13+
expect(html).toContain('&quot;widget_id&quot;');
14+
expect(html).toContain('99');
15+
expect(html).toContain('tool-result');
16+
});
17+
18+
it('uses unknown dispatch key when result_type is missing', () => {
19+
const html = renderToolResultFallback({ payload: 'data' });
20+
expect(html).toContain('Unknown tool result:');
21+
expect(html).toContain('tool-result');
22+
});
23+
24+
it('escapes HTML in JSON fallback body', () => {
25+
const html = mountToolResult({
26+
result_type: 'mystery',
27+
content: XSS_SCRIPT,
28+
});
29+
expectNoRawHtml(html, [XSS_SCRIPT]);
30+
expect(html).toContain('&lt;script&gt;');
31+
});
32+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderFileEditResult } from './file_edit.js';
3+
import { mountToolResult, expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';
4+
5+
describe('renderFileEditResult', () => {
6+
it('renders edited file path in summary', () => {
7+
const html = renderFileEditResult({
8+
result_type: 'file_edit',
9+
file_path: 'src/app.js',
10+
});
11+
expect(html).toContain('Edited: src/app.js');
12+
expect(html).toContain('tool-result');
13+
});
14+
15+
it('handles missing file path', () => {
16+
const html = renderFileEditResult({ result_type: 'file_edit' });
17+
expect(html).toContain('Edited:');
18+
expect(html).not.toContain('undefined');
19+
});
20+
21+
it('escapes HTML in file path via registry', () => {
22+
const html = mountToolResult({
23+
result_type: 'file_edit',
24+
file_path: XSS_SCRIPT,
25+
});
26+
expectNoRawHtml(html, [XSS_SCRIPT]);
27+
});
28+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderFileWriteResult } from './file_write.js';
3+
import { mountToolResult, expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';
4+
5+
describe('renderFileWriteResult', () => {
6+
it('renders written file path in summary', () => {
7+
const html = renderFileWriteResult({
8+
result_type: 'file_write',
9+
file_path: 'output/data.json',
10+
});
11+
expect(html).toContain('Wrote: output/data.json');
12+
expect(html).toContain('tool-result');
13+
});
14+
15+
it('handles missing file path', () => {
16+
const html = renderFileWriteResult({ result_type: 'file_write' });
17+
expect(html).toContain('Wrote:');
18+
expect(html).not.toContain('undefined');
19+
});
20+
21+
it('escapes HTML in file path via registry', () => {
22+
const html = mountToolResult({
23+
result_type: 'file_write',
24+
file_path: XSS_SCRIPT,
25+
});
26+
expectNoRawHtml(html, [XSS_SCRIPT]);
27+
});
28+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderGlobResult } from './glob.js';
3+
4+
describe('renderGlobResult', () => {
5+
it('renders file count in summary', () => {
6+
const html = renderGlobResult({
7+
result_type: 'glob',
8+
num_files: 12,
9+
});
10+
expect(html).toContain('Glob: 12 files found');
11+
expect(html).toContain('tool-result');
12+
});
13+
14+
it('shows truncated flag when set', () => {
15+
const html = renderGlobResult({
16+
result_type: 'glob',
17+
num_files: 100,
18+
truncated: true,
19+
});
20+
expect(html).toContain('(truncated)');
21+
});
22+
23+
it('defaults to zero files when num_files is absent', () => {
24+
const html = renderGlobResult({ result_type: 'glob' });
25+
expect(html).toContain('Glob: 0 files found');
26+
});
27+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderGrepResult } from './grep.js';
3+
4+
describe('renderGrepResult', () => {
5+
it('renders file and line counts in summary', () => {
6+
const html = renderGrepResult({
7+
result_type: 'grep',
8+
num_files: 5,
9+
num_lines: 42,
10+
});
11+
expect(html).toContain('Grep: 5 files, 42 lines');
12+
expect(html).toContain('tool-result');
13+
});
14+
15+
it('defaults counts to zero when absent', () => {
16+
const html = renderGrepResult({ result_type: 'grep' });
17+
expect(html).toContain('Grep: 0 files, 0 lines');
18+
});
19+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderPlanResult } from './plan.js';
3+
import { mountToolResult, expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';
4+
5+
describe('renderPlanResult', () => {
6+
it('renders plan file path in summary', () => {
7+
const html = renderPlanResult({
8+
result_type: 'plan',
9+
file_path: '.cursor/plans/sprint.md',
10+
});
11+
expect(html).toContain('Plan: .cursor/plans/sprint.md');
12+
expect(html).toContain('tool-result');
13+
});
14+
15+
it('handles missing file path', () => {
16+
const html = renderPlanResult({ result_type: 'plan' });
17+
expect(html).toContain('Plan:');
18+
expect(html).not.toContain('undefined');
19+
});
20+
21+
it('escapes HTML in file path via registry', () => {
22+
const html = mountToolResult({
23+
result_type: 'plan',
24+
file_path: XSS_SCRIPT,
25+
});
26+
expectNoRawHtml(html, [XSS_SCRIPT]);
27+
});
28+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { renderTaskResult } from './task.js';
3+
import { mountToolResult, expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';
4+
5+
describe('renderTaskResult', () => {
6+
it('renders completed task with duration and token stats', () => {
7+
const html = renderTaskResult({
8+
result_type: 'task',
9+
status: 'completed',
10+
total_duration_ms: 2500,
11+
total_tokens: 1500,
12+
total_tool_use_count: 3,
13+
});
14+
expect(html).toContain('Task completed');
15+
expect(html).toContain('2.5s');
16+
expect(html).toContain('1,500 tokens');
17+
expect(html).toContain('3 tool calls');
18+
expect(html).toContain('tool-result');
19+
});
20+
21+
it('prefers retrieval status summary when set', () => {
22+
const html = renderTaskResult({
23+
result_type: 'task',
24+
retrieval_status: 'found',
25+
status: 'completed',
26+
});
27+
expect(html).toContain('Task retrieval: found');
28+
expect(html).not.toContain('Task completed');
29+
});
30+
31+
it('prefers description summary when set', () => {
32+
const html = renderTaskResult({
33+
result_type: 'task',
34+
description: 'explore auth module',
35+
});
36+
expect(html).toContain('Task launched: explore auth module');
37+
});
38+
39+
it('escapes HTML in description via registry', () => {
40+
const html = mountToolResult({
41+
result_type: 'task',
42+
description: XSS_SCRIPT,
43+
});
44+
expectNoRawHtml(html, [XSS_SCRIPT]);
45+
});
46+
});

0 commit comments

Comments
 (0)