package tools import ( "context" "encoding/json" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" "isphere-ai-bridge/internal/msglib" ) 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 TestISphereSearchGroupsToolUsesInjectedDisplayEntities(t *testing.T) { fakeMessages := &fakeReceiveMessagesSource{} fakeDisplay := &fakeDisplayEntitySource{entities: []msglib.DisplayEntity{{ EntityType: msglib.EntityTypeGroups, SourceTable: "TD_WorkGroupAuth", JID: "project-room@conference", DisplayName: "Project Room", Confidence: 0.9, MatchedColumns: []string{"GroupJID", "GroupName"}, }}} session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereGroupToolsWithDisplayEntities(server, fakeMessages, fakeDisplay) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchGroups, Arguments: map[string]any{"query": "project", "limit": 5}, }) if err != nil { t.Fatalf("call %s: %v", ToolNameSearchGroups, err) } if callResult.IsError { t.Fatalf("call result is error: %+v", callResult) } var decoded struct { Groups []struct { GroupID string `json:"group_id"` DisplayName string `json:"display_name"` Source string `json:"source"` Confidence float64 `json:"confidence"` RawRef string `json:"raw_ref"` } `json:"groups"` } payload, _ := json.Marshal(callResult.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode structured content %s: %v", payload, err) } if len(decoded.Groups) != 1 { t.Fatalf("groups = %+v, want one", decoded.Groups) } group := decoded.Groups[0] if group.GroupID != "project-room@conference" || group.DisplayName != "Project Room" { t.Fatalf("unexpected MsgLib group identity: %+v", group) } if group.Source != "msglib_readonly" || group.Confidence != 0.9 || group.RawRef != "msglib:TD_WorkGroupAuth" { t.Fatalf("unexpected MsgLib group metadata: %+v", group) } if len(fakeDisplay.queries) != 1 || fakeDisplay.queries[0].EntityType != msglib.EntityTypeGroups || fakeDisplay.queries[0].Query != "project" || fakeDisplay.queries[0].Limit != 5 { t.Fatalf("display queries = %+v", fakeDisplay.queries) } } func TestISphereSearchGroupsToolRanksExactAndDeduplicatesDisplayEntities(t *testing.T) { fakeMessages := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ ID: "msg-1", ConversationType: "groupchat", SenderID: "target@conference.imopenfire1-lanzhou", ReceiverID: "member@imopenfire1-lanzhou", }}}, } fakeDisplay := &fakeDisplayEntitySource{entities: []msglib.DisplayEntity{ {EntityType: msglib.EntityTypeGroups, SourceTable: "TD_WorkGroupAuth", JID: "zzz-target@conference.imopenfire1-lanzhou", DisplayName: "ZZZ Target Group", Confidence: 0.8}, {EntityType: msglib.EntityTypeGroups, SourceTable: "TD_WorkGroupAuth", JID: "target@conference.imopenfire1-lanzhou", DisplayName: "Target Group", Confidence: 0.9}, }} session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereGroupToolsWithDisplayEntities(server, fakeMessages, fakeDisplay) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSearchGroups, Arguments: map[string]any{"query": "target@conference.imopenfire1-lanzhou", "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 { Groups []struct { GroupID string `json:"group_id"` Source string `json:"source"` RawRef string `json:"raw_ref"` } `json:"groups"` } payload, _ := json.Marshal(callResult.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode structured content %s: %v", payload, err) } if len(decoded.Groups) != 2 { t.Fatalf("groups = %+v, want exact and substring only", decoded.Groups) } if decoded.Groups[0].GroupID != "target@conference.imopenfire1-lanzhou" || decoded.Groups[0].Source != "msglib_readonly" || decoded.Groups[0].RawRef != "msglib:TD_WorkGroupAuth" { t.Fatalf("first group should be exact MsgLib match with source metadata, got %+v", decoded.Groups[0]) } if decoded.Groups[1].GroupID != "zzz-target@conference.imopenfire1-lanzhou" { t.Fatalf("second group should be substring match, got %+v", decoded.Groups[1]) } } 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") } }