package tools import ( "context" "encoding/json" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" ) func TestISphereSearchGroupsToolReturnsJIDGroups(t *testing.T) { fake := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ ID: "msg-group-1", ConversationType: "groupchat", SenderID: "project-room@conference.imopenfire1-lanzhou", ReceiverID: "user@imopenfire1-lanzhou", }}}, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereGroupTools(server, fake) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchGroups, Arguments: map[string]any{"query": "project", "limit": 10}, }) if err != nil { t.Fatalf("call %s: %v", ToolNameSearchGroups, err) } if callResult.IsError { t.Fatalf("call result is error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` Groups []struct { GroupID string `json:"group_id"` DisplayName string `json:"display_name"` MemberCount *int `json:"member_count"` OwnerRef *string `json:"owner_ref"` Source string `json:"source"` Confidence float64 `json:"confidence"` RawRef string `json:"raw_ref"` } `json:"groups"` 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.Groups) != 1 { t.Fatalf("groups = %+v, want one", decoded.Groups) } group := decoded.Groups[0] if group.GroupID != "project-room@conference.imopenfire1-lanzhou" || group.DisplayName != "project-room@conference.imopenfire1-lanzhou" { t.Fatalf("unexpected group identity: %+v", group) } if group.MemberCount != nil || group.OwnerRef != nil { t.Fatalf("member/owner should be unknown: %+v", group) } if group.Source != "local_readonly" || group.Confidence != 0.6 || group.RawRef != "groupchat_jid" { t.Fatalf("unexpected group metadata: %+v", group) } if decoded.Audit["tool"] != ToolNameSearchGroups || 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 TestISphereSearchGroupsToolValidatesContractArgs(t *testing.T) { fake := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ ID: "msg-group-1", ConversationType: "groupchat", SenderID: "project-room@conference.imopenfire1-lanzhou", ReceiverID: "user@imopenfire1-lanzhou", }}}, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereGroupTools(server, fake) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchGroups, Arguments: map[string]any{ "query": "project", "source_preference": "local_readonly", "include_archived": 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: ToolNameSearchGroups, Arguments: map[string]any{"query": "project", "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: ToolNameSearchGroups, Arguments: map[string]any{"query": "project", "cursor": "next-page"}, }) if err == nil && !cursorResult.IsError { t.Fatalf("non-empty cursor was accepted before pagination exists") } }