1import { expect, it } from "@effect/vitest";
2import { Tool } from "effect/ai";
3
4import { PreviewToolkit } from "./tools.ts";
5
6const schemaHasDescription = (schema: unknown): boolean => {
7 if (!schema || typeof schema !== "object") return false;
8 const record = schema as Record<string, unknown>;
9 if (typeof record.description === "string" && record.description.length > 0) return true;
10 return [record.anyOf, record.oneOf, record.allOf]
11 .filter(Array.isArray)
12 .some((members) => members.some(schemaHasDescription));
13};
14
15const schemaHasMultipleAllOfDescriptions = (schema: unknown): boolean => {
16 if (!schema || typeof schema !== "object") return false;
17 const record = schema as Record<string, unknown>;
18 const allOf = Array.isArray(record.allOf) ? record.allOf : [];
19 const descriptionCount = allOf.filter(
20 (member) =>
21 member !== null &&
22 typeof member === "object" &&
23 typeof (member as Record<string, unknown>).description === "string",
24 ).length;
25 return descriptionCount > 1 || Object.values(record).some(schemaHasMultipleAllOfDescriptions);
26};
27
28it("exports provider-compatible object schemas with described parameters", () => {
29 for (const tool of Object.values(PreviewToolkit.tools)) {
30 const schema = Tool.getJsonSchema(tool) as {
31 readonly type?: unknown;
32 readonly properties?: Readonly<Record<string, unknown>>;
33 readonly anyOf?: unknown;
34 readonly oneOf?: unknown;
35 };
36 expect(
37 tool.description?.length ?? 0,
38 `${tool.name} should have a useful description`,
39 ).toBeGreaterThan(40);
40 expect(schema.type, `${tool.name} must export a top-level object schema`).toBe("object");
41 expect(schema.anyOf, `${tool.name} must not export a root anyOf`).toBeUndefined();
42 expect(schema.oneOf, `${tool.name} must not export a root oneOf`).toBeUndefined();
43 if (tool.name === "preview_navigate") {
44 expect(schemaHasMultipleAllOfDescriptions(schema)).toBe(false);
45 }
46 expect(
47 schema.properties?.tabId,
48 `${tool.name} must allow an explicit collaborative browser tab target`,
49 ).toBeDefined();
50 for (const [field, fieldSchema] of Object.entries(schema.properties ?? {})) {
51 expect(
52 schemaHasDescription(fieldSchema),
53 `${tool.name}.${field} should explain what data the agent must pass`,
54 ).toBe(true);
55 }
56 }
57});
58
59it("exports exact object result schemas for preview actions", () => {
60 const actionNames = [
61 "preview_click",
62 "preview_type",
63 "preview_hover",
64 "preview_drag",
65 "preview_upload",
66 "preview_press",
67 "preview_scroll",
68 "preview_wait_for",
69 ] as const;
70 for (const name of actionNames) {
71 // Effect's tool schemas follow the decoder default since rc.113 and leave
72 // unmodeled result keys open.
73 expect(Tool.getJsonSchemaFromSchema(PreviewToolkit.tools[name].successSchema)).toEqual({
74 type: "object",
75 properties: { toolIcon: expect.any(Object) },
76 additionalProperties: true,
77 description: "The preview action completed successfully.",
78 });
79 }
80});