package tools import ( "context" "encoding/json" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" ) func TestISphereSearchContactsToolReturnsJIDContacts(t *testing.T) { fake := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ ID: "msg-1", SenderID: "alice@imopenfire1-lanzhou", ReceiverID: "bob@imopenfire1-lanzhou", }}}, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereContactTools(server, fake) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchContacts, Arguments: map[string]any{"query": "alice", "limit": 10}, }) if err != nil { t.Fatalf("call %s: %v", ToolNameSearchContacts, err) } if callResult.IsError { t.Fatalf("call result is error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` Contacts []struct { ContactID string `json:"contact_id"` DisplayName string `json:"display_name"` Account string `json:"account"` Source string `json:"source"` Confidence float64 `json:"confidence"` RawRef string `json:"raw_ref"` } `json:"contacts"` NextCursor any `json:"next_cursor"` Audit map[string]any `json:"audit"` } payload, err := json.Marshal(callResult.StructuredContent) if err != nil { t.Fatalf("marshal structured content: %v", err) } if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode structured content %s: %v", payload, err) } if !decoded.OK { t.Fatalf("ok = false in %s", payload) } if len(decoded.Contacts) != 1 { t.Fatalf("contacts = %+v, want one", decoded.Contacts) } contact := decoded.Contacts[0] if contact.ContactID != "alice@imopenfire1-lanzhou" || contact.DisplayName != "alice@imopenfire1-lanzhou" || contact.Account != "alice@imopenfire1-lanzhou" { t.Fatalf("unexpected contact identity: %+v", contact) } if contact.Source != "local_readonly" || contact.Confidence != 0.6 || contact.RawRef != "message_jid" { t.Fatalf("unexpected contact metadata: %+v", contact) } if decoded.Audit["tool"] != ToolNameSearchContacts || decoded.Audit["result"] != "ok" { t.Fatalf("audit = %+v", decoded.Audit) } if len(fake.queries) != 1 || fake.queries[0].Limit != 0 { t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries) } } func TestISphereSearchContactsToolValidatesContractArgs(t *testing.T) { fake := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ ID: "msg-1", SenderID: "alice@imopenfire1-lanzhou", ReceiverID: "bob@imopenfire1-lanzhou", }}}, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereContactTools(server, fake) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchContacts, Arguments: map[string]any{ "query": "alice", "source_preference": "local_readonly", "include_inactive": true, "cursor": "", "limit": 10, }, }) if err != nil { t.Fatalf("call with safe contract args: %v", err) } if callResult.IsError { t.Fatalf("call result is error: %+v", callResult) } bridgeResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchContacts, Arguments: map[string]any{"query": "alice", "source_preference": "bridge"}, }) if err == nil && !bridgeResult.IsError { t.Fatalf("bridge source_preference was accepted before connector exists") } cursorResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchContacts, Arguments: map[string]any{"query": "alice", "cursor": "next-page"}, }) if err == nil && !cursorResult.IsError { t.Fatalf("non-empty cursor was accepted before pagination exists") } }