feat: custom tool renderers

This commit is contained in:
Zoe
2026-09-25 19:46:00 -05:00
parent 3f81f96bda
commit bb5795e00d
18 changed files with 1294 additions and 65 deletions
+33
View File
@@ -0,0 +1,33 @@
import { expect, test } from "bun:test";
import { readStoredToolError, serializeToolError } from "../utils/tool-errors";
const validationError = "Invalid input for tool search: Type validation failed: Value: {\"query\":123}.\nError message: [{\"code\":\"custom\"}]";
test("tool Error instances persist their message, not an empty JSON object", () => {
expect(serializeToolError(new Error(validationError))).toBe(validationError);
expect(serializeToolError(validationError)).toBe(validationError);
expect(serializeToolError({ reason: "invalid input" })).toBe('{"reason":"invalid input"}');
});
test("text validation errors remain text on history replay", () => {
expect(readStoredToolError({ type: "text", value: validationError })).toEqual({
type: "error-text",
value: validationError,
});
});
test("legacy mislabeled JSON errors cannot break subsequent prompts", () => {
expect(readStoredToolError({ type: "json", value: validationError })).toEqual({
type: "error-text",
value: validationError,
});
});
test("preserves actual JSON errors and handles missing errors", () => {
expect(readStoredToolError({ type: "json", value: '{"reason":"bad input"}' })).toEqual({
type: "error-json",
value: { reason: "bad input" },
});
expect(readStoredToolError(null).type).toBe("error-text");
expect(readStoredToolError(undefined).type).toBe("error-text");
});
+169
View File
@@ -0,0 +1,169 @@
import { describe, expect, test } from "bun:test";
import {
domainInitial,
formatDuration,
formatToolValue,
getDomainColor,
getFaviconUrl,
getToolDisplayInfo,
parseToolValue,
readSearchSources,
} from "../app/utils/tool-rendering";
describe("stored tool values", () => {
test("handles missing, text, valid JSON and streaming JSON", () => {
expect(parseToolValue(null)).toBeNull();
expect(parseToolValue({ type: "text", value: "hello" })).toBe("hello");
expect(parseToolValue({ type: "json", value: '{"query":"gold"}' })).toEqual({ query: "gold" });
expect(formatToolValue({ type: "json", value: '{"query":' })).toBe('{"query":');
});
});
describe("search sources", () => {
test("distinguishes unavailable results from empty results", () => {
expect(readSearchSources(null)).toBeNull();
expect(readSearchSources("partial")).toBeNull();
expect(readSearchSources([])).toEqual([]);
});
test("rejects unsafe links, tolerates malformed entries and deduplicates pages", () => {
expect(readSearchSources([
null,
{ link: "javascript:alert(1)" },
{ link: "data:text/html,hello" },
{ link: "/relative" },
{ link: "https://example.com/page#one", title: "Example", snippet: "Evidence" },
{ link: "https://example.com/page#two", title: "Duplicate" },
{ link: "https://other.example/", title: 1 },
])).toEqual([
{ url: "https://example.com/page", hostname: "example.com", title: "Example", snippet: "Evidence" },
{ url: "https://other.example/", hostname: "other.example", title: "other.example", snippet: "" },
]);
});
});
describe("duration formatting", () => {
test("formats duration in seconds with 1 decimal", () => {
const start = new Date("2026-09-25T12:00:00.000Z");
const end = new Date("2026-09-25T12:00:02.100Z");
expect(formatDuration(start, end)).toBe("2.1s");
});
test("formats numeric timestamps", () => {
const now = 1700000000000;
expect(formatDuration(now, now + 1500)).toBe("1.5s");
expect(formatDuration(new Date(now).toISOString(), now + 3200)).toBe("3.2s");
});
test("formats short duration", () => {
const start = new Date("2026-09-25T12:00:00.000Z");
const end = new Date("2026-09-25T12:00:00.400Z");
expect(formatDuration(start, end)).toBe("0.4s");
});
test("returns null for invalid or negative or sub-threshold duration", () => {
expect(formatDuration(null, null)).toBeNull();
const start = new Date("2026-09-25T12:00:02.000Z");
const end = new Date("2026-09-25T12:00:01.000Z");
expect(formatDuration(start, end)).toBeNull();
});
});
describe("tool display info", () => {
test("handles search with results", () => {
const info = getToolDisplayInfo({
toolName: "search",
status: "completed",
input: { type: "json", value: '{"query":"drizzle pg enum best practices"}' },
output: {
type: "json",
value: JSON.stringify([
{ link: "https://orm.drizzle.team/docs/enum", title: "PostgreSQL enums | Drizzle ORM" },
]),
},
});
expect(info.verb).toBe("Searched 1 site");
expect(info.target).toBe('"drizzle pg enum best practices"');
expect(info.icon).toBe("i-mynaui-search");
});
test("handles read file", () => {
const info = getToolDisplayInfo({
toolName: "read_file",
status: "completed",
input: { type: "json", value: '{"path":"drizzle/schema.ts"}' },
});
expect(info.verb).toBe("Read");
expect(info.target).toBe("drizzle/schema.ts");
expect(info.icon).toBe("i-mynaui-file");
});
test("handles file edit pending", () => {
const info = getToolDisplayInfo({
toolName: "edit_file",
status: "pending",
input: { type: "json", value: '{"path":"server/api/chat/generate.ts"}' },
});
expect(info.verb).toBe("Editing");
expect(info.target).toBe("server/api/chat/generate.ts");
expect(info.icon).toBe("i-mynaui-edit");
});
test("handles query failed", () => {
const info = getToolDisplayInfo({
toolName: "query",
status: "failed",
input: { type: "json", value: '{"query":"select * from tool_calls"}' },
error: { type: "text", value: 'relation "tool_calls" does not exist' },
});
expect(info.verb).toBe("Query failed");
expect(info.target).toBe("select * from tool_calls");
expect(info.icon).toBe("i-mynaui-database");
});
test("handles fetch cancelled", () => {
const info = getToolDisplayInfo({
toolName: "fetchUrl",
status: "cancelled",
input: { type: "json", value: '{"url":"https://orm.drizzle.team/docs/..."}' },
});
expect(info.verb).toBe("Fetch cancelled");
expect(info.target).toBe("https://orm.drizzle.team/docs/...");
expect(info.icon).toBe("i-mynaui-link");
});
test("handles python execution", () => {
const info = getToolDisplayInfo({
toolName: "python",
status: "completed",
input: { type: "json", value: '{"code":"2 + 2"}' },
output: { type: "json", value: '{"output":"4"}' },
});
expect(info.verb).toBe("Ran Python");
expect(info.target).toBe("2 + 2");
expect(info.icon).toBe("i-mynaui-terminal");
});
});
describe("domain utilities", () => {
test("extracts domain initial correctly", () => {
expect(domainInitial("goldprice.org")).toBe("G");
expect(domainInitial("www.kitco.com")).toBe("K");
expect(domainInitial("apmex.com")).toBe("A");
});
test("returns consistent domain colors", () => {
const color1 = getDomainColor("goldprice.org");
const color2 = getDomainColor("goldprice.org");
expect(color1).toBe(color2);
expect(typeof color1).toBe("string");
expect(color1.startsWith("#")).toBe(true);
});
test("formats DuckDuckGo favicon service url", () => {
expect(getFaviconUrl("goldprice.org")).toBe("https://icons.duckduckgo.com/ip3/goldprice.org.ico");
expect(getFaviconUrl("www.kitco.com")).toBe("https://icons.duckduckgo.com/ip3/kitco.com.ico");
});
});