feat: custom tool renderers
This commit is contained in:
@@ -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");
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user