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