Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions static/js/render/registry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ describe('TOOL_USE_RENDERERS', () => {
}
});

it('CORE_TOOL_USE stays in sync with registry keys', () => {
expect(new Set(CORE_TOOL_USE)).toEqual(new Set(Object.keys(TOOL_USE_RENDERERS)));
});

it('does not register the unknown dispatch sentinel as a tool renderer', () => {
expect(Object.prototype.hasOwnProperty.call(TOOL_USE_RENDERERS, UNKNOWN_DISPATCH_KEY)).toBe(false);
});
Expand Down Expand Up @@ -69,6 +73,10 @@ describe('TOOL_RESULT_RENDERERS', () => {
}
});

it('CORE_TOOL_RESULT stays in sync with registry keys', () => {
expect(new Set(CORE_TOOL_RESULT)).toEqual(new Set(Object.keys(TOOL_RESULT_RENDERERS)));
});

it('renderBashResult escapes stdout', () => {
const html = renderToolResult({
result_type: 'bash',
Expand Down Expand Up @@ -188,6 +196,56 @@ describe('renderTodoWriteResult', () => {
});
});

/** Representative fixtures for registry-driven behavioral smoke tests. */
const TOOL_USE_FIXTURES = {
Bash: { input: { command: 'echo hi', description: 'say hi' } },
Read: { input: { file_path: 'README.md' } },
Write: { input: { file_path: 'out.txt', content: 'data' } },
Edit: { input: { file_path: 'a.js', old_string: 'x', new_string: 'y' } },
Glob: { input: { pattern: '*.js', path: 'src' } },
Grep: { input: { pattern: 'TODO', path: 'lib' } },
Task: { input: { subagent_type: 'explore', description: 'scan', prompt: 'go' } },
TodoWrite: { input: { todos: [{ status: 'pending', content: 'task' }] } },
AskUserQuestion: { input: { questions: [{ question: 'OK?' }] } },
WebFetch: { input: { url: 'https://example.com' } },
WebSearch: { input: { query: 'vitest' } },
};

const TOOL_RESULT_FIXTURES = {
bash: { exit_code: 0, stdout: 'ok' },
file_read: { file_path: '/a.txt', num_lines: 10 },
file_edit: { file_path: 'b.js' },
file_write: { file_path: 'c.txt' },
glob: { num_files: 3 },
grep: { num_files: 2, num_lines: 5 },
web_search: { query: 'test', result_count: 1 },
web_fetch: { url: 'https://x.com', status_code: 200 },
task: { status: 'completed', total_duration_ms: 1000 },
todo_write: { todos: [{ status: 'pending', content: 'x' }] },
user_input: { questions: [{ question: 'Q' }], answers: { Q: 'A' } },
plan: { file_path: 'plan.md' },
};

describe('registry behavioral smoke tests', () => {
it('every registered tool_use renderer produces non-empty HTML', () => {
for (const name of CORE_TOOL_USE) {
expect(TOOL_USE_FIXTURES[name], name).toBeDefined();
const html = renderToolUse({ name, ...TOOL_USE_FIXTURES[name] });
Comment thread
clean6378-max-it marked this conversation as resolved.
expect(html, name).toContain('tool-call');
expect(html.length, name).toBeGreaterThan(20);
}
});

it('every registered tool_result renderer produces non-empty HTML', () => {
for (const rt of CORE_TOOL_RESULT) {
expect(TOOL_RESULT_FIXTURES[rt], rt).toBeDefined();
const html = renderToolResult({ result_type: rt, ...TOOL_RESULT_FIXTURES[rt] });
Comment thread
clean6378-max-it marked this conversation as resolved.
expect(html, rt).toContain('tool-result');
expect(html.length, rt).toBeGreaterThan(20);
}
});
});

describe('renderToolResult fallback', () => {
it('renders raw result type and JSON payload for unknown result types', () => {
const html = renderToolResult({
Expand Down
52 changes: 52 additions & 0 deletions static/js/render/test_helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest';
import { renderToolResult } from './registry.js';

/** Common XSS payloads for renderer escaping assertions. */
export const XSS_SCRIPT = '<script>alert(1)</script>';
export const XSS_IMG = '<img onerror=alert(1)>';

/**
* Assert raw HTML fragments are escaped (not present verbatim).
*/
export function expectNoRawHtml(html, rawFragments) {
for (const frag of rawFragments) {
expect(html).not.toContain(frag);
}
}

/**
* Assert an HTML-escaped fragment appears in output.
*/
export function expectEscaped(html, escapedFragment) {
expect(html).toContain(escapedFragment);
}

/**
* Shared behavioral tests for summary-only tool_result renderers (Edited/Plan/Wrote).
*/
export function describeSummaryOnlyResult(
render,
{ suiteName, resultType, label, samplePath },
) {
describe(suiteName, () => {
it(`renders ${label.toLowerCase()} file path in summary`, () => {
const html = render({ result_type: resultType, file_path: samplePath });
expect(html).toContain(`${label}: ${samplePath}`);
expect(html).toContain('tool-result');
});

it('handles missing file path', () => {
const html = render({ result_type: resultType });
expect(html).toContain(`${label}:`);
expect(html).not.toContain('undefined');
});

it('escapes HTML in file path via registry', () => {
const html = renderToolResult({
result_type: resultType,
file_path: XSS_SCRIPT,
});
expectNoRawHtml(html, [XSS_SCRIPT]);
});
});
}
46 changes: 46 additions & 0 deletions static/js/render/tool_result/bash.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import { renderBashResult } from './bash.js';
import { renderToolResult } from '../registry.js';
import { expectNoRawHtml, XSS_IMG } from '../test_helpers.js';

describe('renderBashResult', () => {
it('renders success status with stdout', () => {
const html = renderBashResult({
result_type: 'bash',
exit_code: 0,
stdout: 'hello\nworld',
});
expect(html).toContain('Bash Result (success)');
expect(html).toContain('hello');
expect(html).toContain('stdout');
expect(html).toContain('tool-result');
});

it('renders error status with stderr', () => {
const html = renderBashResult({
result_type: 'bash',
exit_code: 1,
is_error: true,
stderr: 'command failed',
});
expect(html).toContain('error (exit 1)');
expect(html).toContain('command failed');
expect(html).toContain('stderr');
});

it('renders interrupted status', () => {
const html = renderBashResult({ result_type: 'bash', interrupted: true });
expect(html).toContain('Bash Result (interrupted)');
});

it('escapes HTML in stdout and stderr', () => {
const html = renderToolResult({
result_type: 'bash',
exit_code: 0,
stdout: XSS_IMG,
stderr: '<script>x</script>',
});
expectNoRawHtml(html, [XSS_IMG, '<script>']);
expect(html).toContain('&lt;img');
});
});
33 changes: 33 additions & 0 deletions static/js/render/tool_result/fallback.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { renderToolResultFallback } from './fallback.js';
import { renderToolResult } from '../registry.js';
import { expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';

describe('renderToolResultFallback', () => {
it('renders unknown result type and JSON payload', () => {
const html = renderToolResultFallback({
result_type: 'custom_widget',
widget_id: 99,
label: 'test',
});
expect(html).toContain('Unknown tool result: custom_widget');
expect(html).toContain('&quot;widget_id&quot;');
expect(html).toContain('99');
expect(html).toContain('tool-result');
});

it('uses unknown dispatch key when result_type is missing', () => {
const html = renderToolResultFallback({ payload: 'data' });
expect(html).toContain('Unknown tool result:');
expect(html).toContain('tool-result');
});

it('escapes HTML in JSON fallback body', () => {
const html = renderToolResult({
result_type: 'mystery',
content: XSS_SCRIPT,
});
expectNoRawHtml(html, [XSS_SCRIPT]);
expect(html).toContain('&lt;script&gt;');
});
});
9 changes: 9 additions & 0 deletions static/js/render/tool_result/file_edit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { renderFileEditResult } from './file_edit.js';
import { describeSummaryOnlyResult } from '../test_helpers.js';

describeSummaryOnlyResult(renderFileEditResult, {
suiteName: 'renderFileEditResult',
resultType: 'file_edit',
label: 'Edited',
samplePath: 'src/app.js',
});
29 changes: 29 additions & 0 deletions static/js/render/tool_result/file_write.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { renderFileWriteResult } from './file_write.js';
import { renderToolResult } from '../registry.js';
import { expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';

describe('renderFileWriteResult', () => {
it('renders written file path in summary', () => {
const html = renderFileWriteResult({
result_type: 'file_write',
file_path: 'output/data.json',
});
expect(html).toContain('Wrote: output/data.json');
expect(html).toContain('tool-result');
});

it('handles missing file path', () => {
const html = renderFileWriteResult({ result_type: 'file_write' });
expect(html).toContain('Wrote:');
expect(html).not.toContain('undefined');
});

it('escapes HTML in file path via registry', () => {
const html = renderToolResult({
result_type: 'file_write',
file_path: XSS_SCRIPT,
});
expectNoRawHtml(html, [XSS_SCRIPT]);
});
});
27 changes: 27 additions & 0 deletions static/js/render/tool_result/glob.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { renderGlobResult } from './glob.js';

describe('renderGlobResult', () => {
it('renders file count in summary', () => {
const html = renderGlobResult({
result_type: 'glob',
num_files: 12,
});
expect(html).toContain('Glob: 12 files found');
expect(html).toContain('tool-result');
});

it('shows truncated flag when set', () => {
const html = renderGlobResult({
result_type: 'glob',
num_files: 100,
truncated: true,
});
expect(html).toContain('(truncated)');
});

it('defaults to zero files when num_files is absent', () => {
const html = renderGlobResult({ result_type: 'glob' });
expect(html).toContain('Glob: 0 files found');
});
});
19 changes: 19 additions & 0 deletions static/js/render/tool_result/grep.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { renderGrepResult } from './grep.js';

describe('renderGrepResult', () => {
it('renders file and line counts in summary', () => {
const html = renderGrepResult({
result_type: 'grep',
num_files: 5,
num_lines: 42,
});
expect(html).toContain('Grep: 5 files, 42 lines');
expect(html).toContain('tool-result');
});

it('defaults counts to zero when absent', () => {
const html = renderGrepResult({ result_type: 'grep' });
expect(html).toContain('Grep: 0 files, 0 lines');
});
});
9 changes: 9 additions & 0 deletions static/js/render/tool_result/plan.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { renderPlanResult } from './plan.js';
import { describeSummaryOnlyResult } from '../test_helpers.js';

describeSummaryOnlyResult(renderPlanResult, {
suiteName: 'renderPlanResult',
resultType: 'plan',
label: 'Plan',
samplePath: '.cursor/plans/sprint.md',
});
48 changes: 48 additions & 0 deletions static/js/render/tool_result/task.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { renderTaskResult } from './task.js';
import { renderToolResult } from '../registry.js';
import { expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';

describe('renderTaskResult', () => {
it('renders completed task with duration and token stats', () => {
const totalTokens = 1500;
const html = renderTaskResult({
result_type: 'task',
status: 'completed',
total_duration_ms: 2500,
total_tokens: totalTokens,
total_tool_use_count: 3,
});
expect(html).toContain('Task completed');
expect(html).toContain('2.5s');
expect(html).toContain(`${totalTokens.toLocaleString()} tokens`);
expect(html).toContain('3 tool calls');
expect(html).toContain('tool-result');
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('prefers retrieval status summary when set', () => {
const html = renderTaskResult({
result_type: 'task',
retrieval_status: 'found',
status: 'completed',
});
expect(html).toContain('Task retrieval: found');
expect(html).not.toContain('Task completed');
});

it('prefers description summary when set', () => {
const html = renderTaskResult({
result_type: 'task',
description: 'explore auth module',
});
expect(html).toContain('Task launched: explore auth module');
});

it('escapes HTML in description via registry', () => {
const html = renderToolResult({
result_type: 'task',
description: XSS_SCRIPT,
});
expectNoRawHtml(html, [XSS_SCRIPT]);
});
});
39 changes: 39 additions & 0 deletions static/js/render/tool_result/todo_write.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { renderTodoWriteResult } from './todo_write.js';
import { renderToolResult } from '../registry.js';
import { expectNoRawHtml, XSS_SCRIPT } from '../test_helpers.js';

describe('renderTodoWriteResult', () => {
it('renders todo items with emoji status icons', () => {
const html = renderTodoWriteResult({
result_type: 'todo_write',
todos: [
{ status: 'pending', content: 'write tests' },
{ status: 'completed', content: 'fix bug' },
{ status: 'in_progress', content: 'review PR' },
],
});
expect(html).toContain('Todos updated (3 items)');
expect(html).toContain('write tests');
expect(html).toContain('fix bug');
expect(html).toContain('review PR');
expect(html).toContain('tool-result');
});

it('uses todo_count when todos array is absent', () => {
const html = renderTodoWriteResult({
result_type: 'todo_write',
todo_count: 5,
});
expect(html).toContain('Todos updated (5 items)');
});

it('escapes HTML in todo content', () => {
const html = renderToolResult({
result_type: 'todo_write',
todos: [{ status: 'pending', content: XSS_SCRIPT }],
});
expectNoRawHtml(html, [XSS_SCRIPT]);
expect(html).toContain('&lt;script&gt;');
});
});
Loading
Loading