123 lines
4.5 KiB
TypeScript
123 lines
4.5 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { z } from "zod/v4";
|
|
|
|
import { browserToolContracts, normalizeAssistantAppIconUrl } from "./assistant-tool-contracts";
|
|
|
|
const supportedToolArguments = {
|
|
ask_user: {
|
|
question: "How should the app be added?",
|
|
options: [
|
|
{ id: "yes", label: "Yes" },
|
|
{ id: "no", label: "No" },
|
|
],
|
|
},
|
|
configure_app: {
|
|
name: "Wikipedia",
|
|
description: "The Free Encyclopedia",
|
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/wikipedia.svg",
|
|
href: "https://wikipedia.org",
|
|
pingUrl: "https://wikipedia.org",
|
|
},
|
|
configure_board_settings: {
|
|
boardId: "board-1",
|
|
boardName: "Home",
|
|
summary: "Improve dashboard contrast.",
|
|
changes: { primaryColor: "#7C3AED" },
|
|
},
|
|
navigate_to_route: { path: "/manage/apps" },
|
|
open_command_menu: {},
|
|
open_media_request_search: {},
|
|
} satisfies Record<keyof typeof browserToolContracts, unknown>;
|
|
|
|
describe("assistant human tool contracts", () => {
|
|
test("keeps a valid fixture and JSON schema for every supported browser tool", () => {
|
|
expect(Object.keys(supportedToolArguments).toSorted()).toEqual(Object.keys(browserToolContracts).toSorted());
|
|
|
|
for (const [toolName, contract] of Object.entries(browserToolContracts)) {
|
|
const args = supportedToolArguments[toolName as keyof typeof supportedToolArguments];
|
|
expect(contract.parameters.safeParse(args).success, toolName).toBe(true);
|
|
expect(() => z.toJSONSchema(contract.parameters), toolName).not.toThrow();
|
|
}
|
|
});
|
|
|
|
test("accepts a structured question with reusable choices", () => {
|
|
expect(
|
|
browserToolContracts.ask_user.parameters.safeParse({
|
|
question: "How should the app be added?",
|
|
options: [
|
|
{ id: "yes", label: "Yes" },
|
|
{ id: "no", label: "No" },
|
|
{ id: "alternative", label: "Choose an alternative", description: "Adjust the proposed app first." },
|
|
],
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
test("rejects questions without enough choices", () => {
|
|
expect(
|
|
browserToolContracts.ask_user.parameters.safeParse({
|
|
question: "Continue?",
|
|
options: [{ id: "yes", label: "Yes" }],
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
test("allows partial app defaults for the native form", () => {
|
|
expect(
|
|
browserToolContracts.configure_app.parameters.safeParse({
|
|
name: "YouTube",
|
|
href: "https://youtube.com",
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
test("requires an app name before opening the native form", () => {
|
|
expect(
|
|
browserToolContracts.configure_app.parameters.safeParse({
|
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/wikipedia.svg",
|
|
href: "https://wikipedia.org",
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
test("accepts a scoped board settings proposal", () => {
|
|
expect(
|
|
browserToolContracts.configure_board_settings.parameters.safeParse({
|
|
boardId: "board-1",
|
|
boardName: "Home",
|
|
summary: "Improve dashboard contrast and tighten card spacing.",
|
|
changes: {
|
|
primaryColor: "#7C3AED",
|
|
customCss: ".mantine-Card-root { padding: 0.75rem; }",
|
|
},
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
test("rejects invalid board colors and oversized CSS", () => {
|
|
expect(
|
|
browserToolContracts.configure_board_settings.parameters.safeParse({
|
|
boardId: "board-1",
|
|
boardName: "Home",
|
|
summary: "Change styling",
|
|
changes: { primaryColor: "purple", customCss: "a".repeat(16_385) },
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
test("converts the board review parameters to a provider JSON schema", () => {
|
|
expect(() => z.toJSONSchema(browserToolContracts.configure_board_settings.parameters)).not.toThrow();
|
|
});
|
|
|
|
test("only preserves usable app icon URLs", () => {
|
|
expect(normalizeAssistantAppIconUrl("youtube")).toBe("");
|
|
expect(normalizeAssistantAppIconUrl("/api/media/youtube.svg")).toBe("/api/media/youtube.svg");
|
|
expect(normalizeAssistantAppIconUrl("https://icons.example/youtube.svg")).toBe("https://icons.example/youtube.svg");
|
|
expect(normalizeAssistantAppIconUrl("http://icons.example/youtube.svg")).toBe("http://icons.example/youtube.svg");
|
|
expect(normalizeAssistantAppIconUrl("//icons.example/youtube.svg")).toBe("");
|
|
expect(normalizeAssistantAppIconUrl("javascript:alert(1)")).toBe("");
|
|
expect(normalizeAssistantAppIconUrl("data:image/svg+xml,<svg />")).toBe("");
|
|
expect(normalizeAssistantAppIconUrl("file:///etc/passwd")).toBe("");
|
|
});
|
|
});
|