Skip to content

Commit 6a01ac5

Browse files
fix(frontend): harden render registry dispatch and test toolResultHasBody
Introduce UNKNOWN_DISPATCH_KEY sentinel with documented non-registration rule. Guard renderToolUse/renderToolResult against null input; guard toolResultHasBody similarly. Add Vitest coverage for sentinel, null args, and toolResultHasBody contract used by sessions.js.
1 parent 2c1bedd commit 6a01ac5

6 files changed

Lines changed: 64 additions & 5 deletions

File tree

static/js/render/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Sentinel when tool.name or result_type is missing.
3+
* Used for fallback routing only — do not add UNKNOWN_DISPATCH_KEY to TOOL_*_RENDERERS.
4+
*/
5+
export const UNKNOWN_DISPATCH_KEY = 'unknown';

static/js/render/registry.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { renderWebFetchUse } from './tool_use/web_fetch.js';
1111
import { renderWebSearchUse } from './tool_use/web_search.js';
1212
import { renderToolUseFallback } from './tool_use/fallback.js';
1313
import { getToolSummary } from './tool_use/summary.js';
14+
import { UNKNOWN_DISPATCH_KEY } from './constants.js';
1415

1516
import { renderBashResult } from './tool_result/bash.js';
1617
import { renderFileReadResult } from './tool_result/file_read.js';
@@ -71,11 +72,13 @@ function getToolResultRenderer(resultType) {
7172
}
7273

7374
export function renderToolUse(tool) {
74-
const name = tool.name || 'unknown';
75+
if (!tool) return '';
76+
const name = tool.name || UNKNOWN_DISPATCH_KEY;
7577
return getToolUseRenderer(name)(tool);
7678
}
7779

7880
export function renderToolResult(parsed) {
79-
const rt = parsed.result_type || 'unknown';
81+
if (!parsed) return '';
82+
const rt = parsed.result_type || UNKNOWN_DISPATCH_KEY;
8083
return getToolResultRenderer(rt)(parsed);
8184
}

static/js/render/registry.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
renderToolUse,
66
renderToolResult,
77
getToolSummary,
8+
toolResultHasBody,
89
} from './registry.js';
10+
import { UNKNOWN_DISPATCH_KEY } from './constants.js';
911
import { renderWebFetchUse } from './tool_use/web_fetch.js';
1012

1113
const CORE_TOOL_USE = ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Task', 'TodoWrite', 'AskUserQuestion', 'WebFetch', 'WebSearch'];
@@ -32,6 +34,10 @@ describe('TOOL_USE_RENDERERS', () => {
3234
}
3335
});
3436

37+
it('does not register the unknown dispatch sentinel as a tool renderer', () => {
38+
expect(Object.prototype.hasOwnProperty.call(TOOL_USE_RENDERERS, UNKNOWN_DISPATCH_KEY)).toBe(false);
39+
});
40+
3541
it('renderBashUse escapes HTML in command', () => {
3642
const html = renderToolUse({
3743
name: 'Bash',
@@ -41,6 +47,11 @@ describe('TOOL_USE_RENDERERS', () => {
4147
expect(html).toContain('<script>');
4248
});
4349

50+
it('returns empty string for null or undefined tool', () => {
51+
expect(renderToolUse(null)).toBe('');
52+
expect(renderToolUse(undefined)).toBe('');
53+
});
54+
4455
it('renderReadUse escapes file path in body and summary', () => {
4556
const html = renderToolUse({
4657
name: 'Read',
@@ -73,6 +84,41 @@ describe('TOOL_RESULT_RENDERERS', () => {
7384
expect(html).toContain('Bash Result (unknown)');
7485
expect(html).not.toContain('undefined');
7586
});
87+
88+
it('returns empty string for null or undefined parsed', () => {
89+
expect(renderToolResult(null)).toBe('');
90+
expect(renderToolResult(undefined)).toBe('');
91+
});
92+
});
93+
94+
describe('toolResultHasBody', () => {
95+
it('returns false for null or undefined', () => {
96+
expect(toolResultHasBody(null)).toBe(false);
97+
expect(toolResultHasBody(undefined)).toBe(false);
98+
});
99+
100+
it('returns true for bash with stdout or stderr', () => {
101+
expect(toolResultHasBody({ result_type: 'bash', stdout: 'ok' })).toBe(true);
102+
expect(toolResultHasBody({ result_type: 'bash', stderr: 'err' })).toBe(true);
103+
expect(toolResultHasBody({ result_type: 'bash' })).toBe(false);
104+
});
105+
106+
it('returns false for summary-only result types', () => {
107+
expect(toolResultHasBody({ result_type: 'file_read', file_path: '/a' })).toBe(false);
108+
expect(toolResultHasBody({ result_type: 'glob', num_files: 3 })).toBe(false);
109+
});
110+
111+
it('returns true for user_input and todo_write with todos', () => {
112+
expect(toolResultHasBody({ result_type: 'user_input' })).toBe(true);
113+
expect(toolResultHasBody({ result_type: 'todo_write', todos: [{ content: 'x' }] })).toBe(true);
114+
expect(toolResultHasBody({ result_type: 'todo_write', todo_count: 1 })).toBe(false);
115+
});
116+
117+
it('returns true for task when duration, retrieval, or description is set', () => {
118+
expect(toolResultHasBody({ result_type: 'task', description: 'subagent' })).toBe(true);
119+
expect(toolResultHasBody({ result_type: 'task', total_duration_ms: 100 })).toBe(true);
120+
expect(toolResultHasBody({ result_type: 'task', status: 'completed' })).toBe(false);
121+
});
76122
});
77123

78124
describe('getToolSummary', () => {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { finishToolResult } from './common.js';
2+
import { UNKNOWN_DISPATCH_KEY } from '../constants.js';
23

34
export function renderToolResultFallback(parsed) {
4-
const rt = parsed.result_type || 'unknown';
5+
const rt = parsed.result_type || UNKNOWN_DISPATCH_KEY;
56
const summary = `Tool result (${rt})`;
67
return finishToolResult(summary, '');
78
}

static/js/render/tool_result/utils.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { UNKNOWN_DISPATCH_KEY } from '../constants.js';
2+
13
export function toolResultHasBody(parsed) {
2-
const rt = parsed.result_type || 'unknown';
4+
if (!parsed) return false;
5+
const rt = parsed.result_type || UNKNOWN_DISPATCH_KEY;
36
if (rt === 'bash') return !!(parsed.stdout || parsed.stderr);
47
if (rt === 'todo_write') return !!(parsed.todos && parsed.todos.length);
58
if (rt === 'user_input') return true;

static/js/render/tool_use/fallback.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { esc, truncate } from '../../shared/utils.js';
22
import { getToolSummary } from './summary.js';
33
import { wrapToolUse } from './common.js';
4+
import { UNKNOWN_DISPATCH_KEY } from '../constants.js';
45

56
export function renderToolUseFallback(tool) {
6-
const name = tool.name || 'unknown';
7+
const name = tool.name || UNKNOWN_DISPATCH_KEY;
78
const inp = tool.input || {};
89
const summary = getToolSummary(name, inp);
910
const s = JSON.stringify(inp, null, 2);

0 commit comments

Comments
 (0)