1package tools
2
3import (
4 "encoding/json"
5 "testing"
6)
7
8// TestToolSchemasAnthropicCompatible guards the wire contract with the
9// Claude API: recent models (Opus 5.5, Fable 5.1) reject composition
10// keywords (oneOf/anyOf/allOf) at the root of a tool input_schema with a
11// 400. Every built-in schema must keep a plain object root.
12func TestToolSchemasAnthropicCompatible(t *testing.T) {
13 schemas := map[string]json.RawMessage{
14 "ast": (&ASTTool{}).Schema(),
15 "bash": (&BashTool{}).Schema(),
16 "create_worktree": (&CreateWorktreeTool{}).Schema(),
17 "edit": (&EditTool{}).Schema(),
18 "glob": (&GlobTool{}).Schema(),
19 "grep": (&GrepTool{}).Schema(),
20 "lsp": (&LSPTool{}).Schema(),
21 "manage_worktrees": (&ManageWorktreesTool{}).Schema(),
22 "plan": (&PlanTool{}).Schema(),
23 "python": (&PythonTool{}).Schema(),
24 "read": (&ReadTool{}).Schema(),
25 "schedule": (&ScheduleTool{}).Schema(),
26 "subagent": (&SubagentTool{}).Schema(),
27 "telegram_send_file": (&TelegramSendFileTool{}).Schema(),
28 "telegram_send_image": (&TelegramSendImageTool{}).Schema(),
29 "update_goal": (&UpdateGoalTool{}).Schema(),
30 "web_click": (&WebClickTool{}).Schema(),
31 "web_find": (&WebFindTool{}).Schema(),
32 "web_open": (&WebOpenTool{}).Schema(),
33 "web_search": (&WebSearchTool{}).Schema(),
34 "worktree": (&WorktreeTool{}).Schema(),
35 "write": (&WriteTool{}).Schema(),
36 }
37 for name, raw := range schemas {
38 var schema map[string]any
39 if err := json.Unmarshal(raw, &schema); err != nil {
40 t.Fatalf("%s: invalid JSON schema: %v", name, err)
41 }
42 for _, key := range []string{"oneOf", "anyOf", "allOf"} {
43 if _, ok := schema[key]; ok {
44 t.Errorf("%s: top-level %q rejected by Claude API", name, key)
45 }
46 }
47 if schema["type"] != "object" {
48 t.Errorf("%s: root type = %v, want object", name, schema["type"])
49 }
50 }
51}
52
53// TestWebOpenExactlyOneRefOrURL keeps the exactly-one-of contract after
54// the oneOf keyword was removed from the web_open schema (Claude API
55// rejects top-level composition). parseWebOpenArgs remains the enforcer.
56func TestWebOpenExactlyOneRefOrURL(t *testing.T) {
57 for _, tc := range []struct {
58 name string
59 raw string
60 want bool
61 refID string
62 url string
63 }{
64 {"ref only", `{"ref_id":"web-1"}`, true, "web-1", ""},
65 {"url only", `{"url":"https://example.com"}`, true, "", "https://example.com"},
66 {"both", `{"ref_id":"web-1","url":"https://example.com"}`, false, "", ""},
67 {"neither", `{}`, false, "", ""},
68 } {
69 t.Run(tc.name, func(t *testing.T) {
70 args, ok := parseWebOpenArgs(json.RawMessage(tc.raw))
71 if ok != tc.want {
72 t.Fatalf("ok = %v, want %v", ok, tc.want)
73 }
74 if ok && (args.refID != tc.refID || args.url != tc.url) {
75 t.Fatalf("args = %+v", args)
76 }
77 })
78 }
79}