package operation import ( "bytes" "encoding/json" "os" "path/filepath" "sort" "testing" ) const updateToolContractEnv = "UPDATE_TOOL_CONTRACT" type toolContract struct { Scope string `json:"scope"` Access string `json:"access"` Name string `json:"name"` Description string `json:"description"` InputSchema any `json:"inputSchema"` Annotations contractAnnotations `json:"annotations"` } type contractAnnotations struct { Title string `json:"title"` ReadOnlyHint bool `json:"readOnlyHint"` DestructiveHint bool `json:"destructiveHint"` IdempotentHint bool `json:"idempotentHint"` OpenWorldHint bool `json:"openWorldHint"` } func TestToolContract(t *testing.T) { const ( wantDomains = 18 wantTools = 54 wantRead = 33 wantWrite = 21 ) contracts := make([]toolContract, 0, wantTools) seenScopes := make(map[string]struct{}, wantDomains) seenNames := make(map[string]struct{}, wantTools) readCount, writeCount := 0, 0 for _, domain := range domainTools { scope := domain.Scope() if scope == "" { t.Fatal("registered tool domain has an empty scope") } if _, duplicate := seenScopes[scope]; duplicate { t.Fatalf("duplicate tool domain scope %q", scope) } seenScopes[scope] = struct{}{} for _, registered := range domain.ReadTools() { contracts = append(contracts, decodeToolContract(t, scope, "read", registered.Tool)) readCount++ } for _, registered := range domain.WriteTools() { contracts = append(contracts, decodeToolContract(t, scope, "write", registered.Tool)) writeCount++ } } if len(seenScopes) != wantDomains { t.Errorf("domain count = %d, want %d", len(seenScopes), wantDomains) } if len(contracts) != wantTools { t.Errorf("tool count = %d, want %d", len(contracts), wantTools) } if readCount != wantRead { t.Errorf("read tool count = %d, want %d", readCount, wantRead) } if writeCount != wantWrite { t.Errorf("write tool count = %d, want %d", writeCount, wantWrite) } for _, contract := range contracts { if _, duplicate := seenNames[contract.Name]; duplicate { t.Errorf("duplicate tool name %q", contract.Name) } seenNames[contract.Name] = struct{}{} } sort.Slice(contracts, func(i, j int) bool { if contracts[i].Scope != contracts[j].Scope { return contracts[i].Scope < contracts[j].Scope } if contracts[i].Access != contracts[j].Access { return contracts[i].Access < contracts[j].Access } return contracts[i].Name < contracts[j].Name }) got, err := json.MarshalIndent(contracts, "", " ") if err != nil { t.Fatalf("marshal tool contract: %v", err) } got = append(got, '\n') goldenPath := filepath.Join("testdata", "tools.golden.json") if os.Getenv(updateToolContractEnv) == "1" { if err := os.WriteFile(goldenPath, got, 0o644); err != nil { t.Fatalf("update tool contract: %v", err) } } want, err := os.ReadFile(goldenPath) if err != nil { t.Fatalf("read tool contract: %v", err) } if !bytes.Equal(got, want) { t.Errorf("tool contract changed; inspect the semantic diff before running %s=1 go test -run '^TestToolContract$' ./operation/", updateToolContractEnv) } } func decodeToolContract(t *testing.T, scope, access string, toolDefinition any) toolContract { t.Helper() data, err := json.Marshal(toolDefinition) if err != nil { t.Fatalf("marshal %s tool in scope %q: %v", access, scope, err) } var definition map[string]any if err := json.Unmarshal(data, &definition); err != nil { t.Fatalf("decode %s tool in scope %q: %v", access, scope, err) } name := requiredString(t, definition, "name", scope) description := requiredString(t, definition, "description", name) inputSchema, ok := definition["inputSchema"].(map[string]any) if !ok { t.Fatalf("tool %q has inputSchema of type %T, want JSON object", name, definition["inputSchema"]) } // An omitted required keyword and an empty array have the same JSON Schema meaning. if _, ok := inputSchema["required"]; !ok { inputSchema["required"] = []any{} } annotations, _ := definition["annotations"].(map[string]any) // Normalize protocol defaults independently of SDK omitempty behavior. return toolContract{ Scope: scope, Access: access, Name: name, Description: description, InputSchema: inputSchema, Annotations: contractAnnotations{ Title: stringField(annotations, "title", ""), ReadOnlyHint: boolField(annotations, "readOnlyHint", false), DestructiveHint: boolField(annotations, "destructiveHint", true), IdempotentHint: boolField(annotations, "idempotentHint", false), OpenWorldHint: boolField(annotations, "openWorldHint", true), }, } } func requiredString(t *testing.T, object map[string]any, key, owner string) string { t.Helper() value, ok := object[key].(string) if !ok || value == "" { t.Fatalf("%s has missing or empty %q", owner, key) } return value } func stringField(object map[string]any, key, fallback string) string { if value, ok := object[key].(string); ok { return value } return fallback } func boolField(object map[string]any, key string, fallback bool) bool { if value, ok := object[key].(bool); ok { return value } return fallback }