package tools import ( "context" "encoding/json" "os" "testing" "switchboard-core/store" ) // TestMain registers late-registered tools (notes) with nil deps so // that Definition()-level tests work. Execute() tests need real deps. func TestMain(m *testing.M) { RegisterNoteTools(store.Stores{}, nil) os.Exit(m.Run()) } // ── Registry Tests ────────────────────────── func TestRegistryHasTools(t *testing.T) { // TestMain registers note tools via RegisterNoteTools if !HasTools() { t.Fatal("Expected HasTools() == true after init registration") } } func TestRegistryAllDefinitions(t *testing.T) { defs := AllDefinitions() if len(defs) < 4 { t.Errorf("Expected at least 4 tool definitions, got %d", len(defs)) } expected := map[string]bool{ "note_create": false, "note_search": false, "note_update": false, "note_list": false, } for _, d := range defs { if _, ok := expected[d.Name]; ok { expected[d.Name] = true } // Every tool must have a description and parameters if d.Description == "" { t.Errorf("Tool %q has empty description", d.Name) } if len(d.Parameters) == 0 { t.Errorf("Tool %q has empty parameters", d.Name) } } for name, found := range expected { if !found { t.Errorf("Expected tool %q not found in registry", name) } } } func TestRegistryGetKnown(t *testing.T) { tool := Get("note_create") if tool == nil { t.Fatal("Get(note_create) returned nil") } def := tool.Definition() if def.Name != "note_create" { t.Errorf("Expected name 'note_create', got %q", def.Name) } } func TestRegistryGetUnknown(t *testing.T) { tool := Get("nonexistent_tool") if tool != nil { t.Error("Get(nonexistent_tool) should return nil") } } func TestExecuteCallUnknownTool(t *testing.T) { ctx := context.Background() execCtx := ExecutionContext{UserID: "test", ChannelID: "test"} call := ToolCall{ID: "call_1", Name: "nonexistent", Arguments: "{}"} result := ExecuteCall(ctx, execCtx, call) if !result.IsError { t.Error("Expected is_error=true for unknown tool") } if result.ToolCallID != "call_1" { t.Errorf("Expected tool_call_id='call_1', got %q", result.ToolCallID) } } func TestExecuteAll(t *testing.T) { ctx := context.Background() execCtx := ExecutionContext{UserID: "test", ChannelID: "test"} calls := []ToolCall{ {ID: "call_1", Name: "nonexistent_a", Arguments: "{}"}, {ID: "call_2", Name: "nonexistent_b", Arguments: "{}"}, } results := ExecuteAll(ctx, execCtx, calls) if len(results) != 2 { t.Fatalf("Expected 2 results, got %d", len(results)) } for i, r := range results { if !r.IsError { t.Errorf("Result %d: expected is_error=true", i) } } } // ── Tool Definition Schema Tests ──────────── func TestToolDefinitionsHaveValidJSON(t *testing.T) { for _, def := range AllDefinitions() { var schema map[string]interface{} if err := json.Unmarshal(def.Parameters, &schema); err != nil { t.Errorf("Tool %q has invalid JSON schema: %v", def.Name, err) } // Should be a JSON Schema object with "type" and "properties" if schema["type"] != "object" { t.Errorf("Tool %q schema type should be 'object', got %v", def.Name, schema["type"]) } if _, ok := schema["properties"]; !ok { t.Errorf("Tool %q schema missing 'properties'", def.Name) } } } // ── JSON Schema Helper Tests ──────────────── func TestPropString(t *testing.T) { p := Prop("string", "a description") if p["type"] != "string" { t.Errorf("Expected type=string, got %v", p["type"]) } if p["description"] != "a description" { t.Errorf("Expected description, got %v", p["description"]) } } func TestPropEnum(t *testing.T) { p := PropEnum("content mode", "replace", "append", "prepend") if p["type"] != "string" { t.Error("Expected type=string") } enums := p["enum"].([]string) if len(enums) != 3 { t.Errorf("Expected 3 enum values, got %d", len(enums)) } } func TestPropArray(t *testing.T) { p := PropArray("list of tags") if p["type"] != "array" { t.Error("Expected type=array") } items := p["items"].(map[string]string) if items["type"] != "string" { t.Error("Expected items.type=string") } } func TestJSONSchema(t *testing.T) { props := map[string]interface{}{ "title": Prop("string", "note title"), "content": Prop("string", "note body"), } schema := JSONSchema(props, []string{"title"}) var m map[string]interface{} if err := json.Unmarshal(schema, &m); err != nil { t.Fatalf("Invalid JSON: %v", err) } if m["type"] != "object" { t.Error("Expected type=object") } required := m["required"].([]interface{}) if len(required) != 1 || required[0] != "title" { t.Errorf("Expected required=[title], got %v", required) } mProps := m["properties"].(map[string]interface{}) if _, ok := mProps["title"]; !ok { t.Error("Missing property 'title'") } if _, ok := mProps["content"]; !ok { t.Error("Missing property 'content'") } } func TestMustJSON(t *testing.T) { result := MustJSON(map[string]string{"key": "value"}) var m map[string]string if err := json.Unmarshal(result, &m); err != nil { t.Fatalf("MustJSON produced invalid JSON: %v", err) } if m["key"] != "value" { t.Errorf("Expected value, got %q", m["key"]) } }