From 298482c25d98e1a801f29395e4e6046380776425 Mon Sep 17 00:00:00 2001 From: zhaoyilun Date: Fri, 10 Jul 2026 08:01:15 +0800 Subject: [PATCH] feat: enrich receive messages with display names --- docs/current-status-card.md | 5 +- .../capability-source-matrix.md | 8 +- ...tage-c-log-backed-receive-messages-loop.md | 69 +++++++++++-- internal/mcpserver/server.go | 2 +- internal/tools/isphere_read.go | 98 ++++++++++++++++++- internal/tools/isphere_read_test.go | 71 ++++++++++++++ 6 files changed, 232 insertions(+), 21 deletions(-) diff --git a/docs/current-status-card.md b/docs/current-status-card.md index b1c0f4a..2f8a504 100644 --- a/docs/current-status-card.md +++ b/docs/current-status-card.md @@ -56,7 +56,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme ## Current loop -Current loop: `Stage C / Loop C32 - receive message display-name enrichment`. +Current loop: `Stage C / Loop C33 - optional real receive-message display smoke`. Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`. @@ -100,10 +100,11 @@ Current loop output so far: 29. C29: added bounded `display_entities` support across the x86 sidecar, Go client, and verification script; evidence-backed verification returned sanitized contact/group counts and source-table names only, with no message bodies, file paths, raw rows, sends, writes, hooks, or injection. 30. C30: wired `DisplayEntities` into `isphere_search_contacts` and `isphere_search_groups` as an optional env-configured enrichment source with fake-source tests; default MCP verification now clears MsgLib env to keep the standard smoke deterministic. 31. C31: added optional `scripts/verify-msglib-mcp-enrichment.ps1`; real copied-DB MCP smoke returned sanitized contact/group counts and MsgLib source refs without printing names, JIDs, message bodies, file paths, or raw rows. +32. C32: `isphere_receive_messages` can now use the optional MsgLib display-entity source to fill safe `sender_name` and `conversation.display_name` fields while preserving message text/content/attachment behavior. ## Next business mainline -1. Run Stage C Loop C32: reuse the optional MsgLib display-entity source to enrich `isphere_receive_messages` sender/conversation display fields without changing message text/body handling. +1. Run Stage C Loop C33: add an optional real copied-DB smoke for receive-message display-name enrichment, keeping output sanitized by default. 2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated. 3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated. 4. Keep UIA selector/report work as fallback support, not as the primary roadmap. diff --git a/docs/source-discovery/capability-source-matrix.md b/docs/source-discovery/capability-source-matrix.md index 136f404..c89e8c0 100644 --- a/docs/source-discovery/capability-source-matrix.md +++ b/docs/source-discovery/capability-source-matrix.md @@ -17,9 +17,9 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md` | MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice | | --- | --- | --- | --- | --- | --- | -| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; copied `MsgLib.db` through a future x86 read-only sidecar/helper | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md` | C20 supports and validates the current safe receive-message contract args; C31 proves real copied-DB MCP contact/group enrichment with sanitized output; DB-backed message listing is still not implemented | C32 enrich receive-message sender/conversation display fields from MsgLib metadata | -| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md` | C31 verifies optional copied-DB MCP contact enrichment with sanitized output | C32 reuse display metadata for receive-message display fields | -| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C31 verifies optional copied-DB MCP group enrichment with sanitized output | C32 reuse display metadata for receive-message conversation display fields | +| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; copied `MsgLib.db` through a future x86 read-only sidecar/helper | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md` | C20 supports and validates the current safe receive-message contract args; C32 wires optional MsgLib display metadata into receive-message `sender_name` and `conversation.display_name`; DB-backed message listing is still not implemented | C33 optional real receive-message display smoke | +| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md` | C32 reuses contact display metadata for receive-message `sender_name`; search enrichment remains available | C33 optional real receive-message display smoke | +| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C32 reuses group display metadata for receive-message `conversation.display_name`; search enrichment remains available | C33 optional real receive-message display smoke | | `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C22 verifies safe file-list contract args; no log-to-cache/download mapping yet | defer download mapping; precheck contact/group display-name evidence | | `isphere_send_message` | none validated yet | existing bridge/API/local service preferred; UIA write connector only after internal-sandbox action evidence; network/protocol connector only after protocol discovery | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | C15 blocked: contract exists, but no committed bridge/API/local service, helper write op, UIA action chain, or protocol connector is validated | do not implement write behavior; harden existing read source configuration | | `isphere_send_file` | none validated yet | depends on send-message connector plus outbound file/upload evidence | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | blocked behind `isphere_send_message` connector selection and file upload/source policy | defer until send-message connector is validated | @@ -39,4 +39,4 @@ Start Stage C with a narrow log-backed `isphere_receive_messages` source abstrac ## Deferred source work -`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, and C31 proves real copied-DB MCP enrichment with sanitized output. The next step is receive-message display-name enrichment from the same metadata source. +`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, and C32 reuses it for receive-message display fields. The next step is an optional real receive-message display smoke. diff --git a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md index 8cde52e..c95d1eb 100644 --- a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md +++ b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md @@ -2419,29 +2419,80 @@ Optional real MsgLib MCP enrichment smoke result: **Goal:** Reuse the optional MsgLib display-entity source to enrich `isphere_receive_messages` sender/conversation display fields, without changing message text/body handling or adding send/download/write behavior. -**Planned files:** +**Files:** - Modify: `internal/tools/isphere_read.go` - Modify: `internal/tools/isphere_read_test.go` - Modify: `internal/mcpserver/server.go` -- Modify: `scripts/verify-go-mcp.ps1` or optional enrichment smoke as needed -- Modify: docs/status/matrix/plan +- Modify: `docs/current-status-card.md` +- Modify: `docs/source-discovery/capability-source-matrix.md` +- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md` **Planned behavior:** - Default behavior remains unchanged without MsgLib env. - With injected display metadata, fill safe display fields such as `sender_name` and `conversation.display_name` when a matching contact/group entity exists. - Do not alter `text`, `content_text`, attachments, raw refs, file paths, or message bodies. -- [ ] **Step 1: Write failing receive-message display-name tests** +- [x] **Step 1: Write failing receive-message display-name tests** -Use fake display metadata to prove sender/conversation display enrichment while preserving existing message fields. +Added `TestISphereReceiveMessagesToolUsesInjectedDisplayEntities`. Verified RED with missing `RegisterISphereReadToolsWithDisplayEntities`. -- [ ] **Step 2: Implement optional display-name enrichment** +- [x] **Step 2: Implement optional display-name enrichment** -Wire display metadata into receive message map output only. +Added read-tool display entity injection. The receive-message map now optionally fills `sender_name` from contact metadata and `conversation.display_name` from group/contact metadata. Message text/content/attachment/raw fields are unchanged. -- [ ] **Step 3: Verify/docs/commit** +- [x] **Step 3: Verify/docs/commit** -Run the standard full gate and update status. +Run before commit: + +```powershell +git diff --check +go test ./... +go build ./cmd/isphere-mcp +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\build-msglib-sidecar.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-msglib-sidecar.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-go-mcp.ps1 +``` + +Final verification passed: `git diff --check`, `go test ./...`, explicit `go build ./cmd/isphere-mcp`, default sidecar smoke, and deterministic `scripts/verify-go-mcp.ps1` with MsgLib env cleared. + +--- + +## Loop C32 Result + +Receive-message display-name enrichment result: + +- `isphere_receive_messages` can use injected/Env MsgLib display metadata. +- `sender_name` can be populated from contact display entities. +- `conversation.display_name` can be populated from group/contact display entities. +- Default behavior remains unchanged when MsgLib env is absent. +- Message `text`, `content_text`, attachment metadata, source, and raw refs remain unchanged. + +--- + +## Loop C33: Optional real receive-message display smoke + +**Goal:** Prove receive-message display-name enrichment through the real copied-DB MsgLib sidecar using an optional smoke that prints only sanitized booleans/counts by default. + +**Planned files:** +- Modify: `scripts/verify-msglib-mcp-enrichment.ps1` or add a focused optional smoke. +- Modify: docs/status/matrix/plan. + +**Planned behavior:** +- Default verification remains deterministic and does not use real MsgLib env. +- Optional smoke may internally use real enriched IDs but should print only booleans/counts/source refs. +- Do not print entity values, message bodies, file paths, or raw rows. + +- [ ] **Step 1: Add optional receive display smoke** + +Use real MsgLib-enriched contact/group metadata internally to construct a synthetic encrypted PacketReader fixture and assert display fields are populated. + +- [ ] **Step 2: Run full and optional verification** + +Run standard gate plus optional smoke with copied DB env. + +- [ ] **Step 3: Update docs and commit** + +Record sanitized evidence and next loop. --- diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go index 4e5ffa0..7f102ba 100644 --- a/internal/mcpserver/server.go +++ b/internal/mcpserver/server.go @@ -89,7 +89,7 @@ func NewServerWithSources(source tools.ReceiveMessagesSource, displaySource tool Version: ServerVersion, }, nil) tools.RegisterWinHelperTools(server, helperclient.Client{}) - tools.RegisterISphereReadTools(server, source) + tools.RegisterISphereReadToolsWithDisplayEntities(server, source, displaySource) tools.RegisterISphereContactToolsWithDisplayEntities(server, source, displaySource) tools.RegisterISphereGroupToolsWithDisplayEntities(server, source, displaySource) tools.RegisterISphereFileTools(server, source) diff --git a/internal/tools/isphere_read.go b/internal/tools/isphere_read.go index 20db803..f667eb8 100644 --- a/internal/tools/isphere_read.go +++ b/internal/tools/isphere_read.go @@ -10,6 +10,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" + "isphere-ai-bridge/internal/msglib" ) const ToolNameReceiveMessages = "isphere_receive_messages" @@ -30,6 +31,10 @@ type ReceiveMessagesArgs struct { } func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) { + RegisterISphereReadToolsWithDisplayEntities(server, source, nil) +} + +func RegisterISphereReadToolsWithDisplayEntities(server *mcp.Server, source ReceiveMessagesSource, displaySource DisplayEntitySource) { if source == nil { source = isphere.EncryptedPacketLogSource{} } @@ -51,8 +56,17 @@ func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) if err != nil { return nil, nil, err } + displayNames := receiveMessageDisplayNames{} + if displaySource != nil && len(result.Messages) > 0 { + loaded, err := loadReceiveMessageDisplayNames(ctx, displaySource) + if err != nil { + return nil, nil, err + } + displayNames = loaded + } return nil, receiveMessagesResultToMap(result, started, time.Now().UTC(), receiveMessagesMapOptions{ IncludeAttachmentMetadata: includeReceiveMessageAttachmentMetadata(input), + DisplayNames: displayNames, }), nil }) } @@ -83,6 +97,12 @@ func includeReceiveMessageAttachmentMetadata(input ReceiveMessagesArgs) bool { type receiveMessagesMapOptions struct { IncludeAttachmentMetadata bool + DisplayNames receiveMessageDisplayNames +} + +type receiveMessageDisplayNames struct { + Contacts map[string]string + Groups map[string]string } func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started time.Time, finished time.Time, options receiveMessagesMapOptions) map[string]any { @@ -92,13 +112,14 @@ func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started ti if options.IncludeAttachmentMetadata { attachments = receiveMessageAttachments(message) } + senderName := nullableDisplayName(options.DisplayNames.Contacts, message.SenderID) messages = append(messages, map[string]any{ "id": message.ID, "message_id": message.ID, "conversation_id": message.ConversationID, "conversation_type": message.ConversationType, "sender_id": message.SenderID, - "sender_name": nil, + "sender_name": senderName, "receiver_id": message.ReceiverID, "text": message.Text, "content_type": receiveMessageContentType(message, attachments), @@ -116,7 +137,7 @@ func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started ti } return map[string]any{ "ok": true, - "conversation": receiveMessagesConversation(result.Messages), + "conversation": receiveMessagesConversation(result.Messages, options.DisplayNames), "messages": messages, "next_cursor": nil, "audit": map[string]any{ @@ -130,18 +151,85 @@ func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started ti } } -func receiveMessagesConversation(messages []isphere.Message) any { +func receiveMessagesConversation(messages []isphere.Message, displayNames receiveMessageDisplayNames) any { if len(messages) == 0 { return nil } first := messages[0] + conversationType := contractConversationType(first.ConversationType) + displayName := first.ConversationID + if conversationType == "group" { + if name := lookupDisplayName(displayNames.Groups, first.SenderID, first.ReceiverID, first.ConversationID); name != "" { + displayName = name + } + } else if name := lookupDisplayName(displayNames.Contacts, first.SenderID, first.ReceiverID); name != "" { + displayName = name + } return map[string]any{ "conversation_id": first.ConversationID, - "conversation_type": contractConversationType(first.ConversationType), - "display_name": first.ConversationID, + "conversation_type": conversationType, + "display_name": displayName, } } +func loadReceiveMessageDisplayNames(ctx context.Context, source DisplayEntitySource) (receiveMessageDisplayNames, error) { + contacts, err := source.DisplayEntities(ctx, msglib.DisplayEntitiesOptions{EntityType: msglib.EntityTypeContacts, Limit: msglib.MaxDisplayEntityLimit}) + if err != nil { + return receiveMessageDisplayNames{}, err + } + groups, err := source.DisplayEntities(ctx, msglib.DisplayEntitiesOptions{EntityType: msglib.EntityTypeGroups, Limit: msglib.MaxDisplayEntityLimit}) + if err != nil { + return receiveMessageDisplayNames{}, err + } + return receiveMessageDisplayNames{ + Contacts: displayEntityNameMap(contacts, msglib.EntityTypeContacts), + Groups: displayEntityNameMap(groups, msglib.EntityTypeGroups), + }, nil +} + +func displayEntityNameMap(entities []msglib.DisplayEntity, entityType string) map[string]string { + names := map[string]string{} + for _, entity := range entities { + if entity.EntityType != "" && entity.EntityType != entityType { + continue + } + jid := strings.ToLower(strings.TrimSpace(entity.JID)) + name := strings.TrimSpace(entity.DisplayName) + if jid == "" || name == "" { + continue + } + if _, exists := names[jid]; !exists { + names[jid] = name + } + } + return names +} + +func nullableDisplayName(names map[string]string, id string) any { + if name := lookupDisplayName(names, id); name != "" { + return name + } + return nil +} + +func lookupDisplayName(names map[string]string, candidates ...string) string { + if len(names) == 0 { + return "" + } + for _, candidate := range candidates { + for _, part := range strings.Split(candidate, "|") { + key := strings.ToLower(strings.TrimSpace(part)) + if key == "" { + continue + } + if name := names[key]; name != "" { + return name + } + } + } + return "" +} + func contractConversationType(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "groupchat", "group": diff --git a/internal/tools/isphere_read_test.go b/internal/tools/isphere_read_test.go index 0b4c0aa..9c8c976 100644 --- a/internal/tools/isphere_read_test.go +++ b/internal/tools/isphere_read_test.go @@ -8,6 +8,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" + "isphere-ai-bridge/internal/msglib" ) func TestISphereReceiveMessagesToolReturnsSourceMessages(t *testing.T) { @@ -142,6 +143,76 @@ func TestISphereReceiveMessagesToolReturnsSourceMessages(t *testing.T) { } } +func TestISphereReceiveMessagesToolUsesInjectedDisplayEntities(t *testing.T) { + fake := &fakeReceiveMessagesSource{ + result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ + ID: "msg-display-1", + ConversationID: "project-room@conference|sender@example", + ConversationType: "groupchat", + SenderID: "sender@example", + ReceiverID: "project-room@conference", + Text: "keep this body unchanged", + Timestamp: "1783423807000", + }}}, + } + fakeDisplay := &fakeDisplayEntitySource{entities: []msglib.DisplayEntity{ + {EntityType: msglib.EntityTypeContacts, SourceTable: "TD_CustomEffigy", JID: "sender@example", DisplayName: "Sender Name", Confidence: 0.9}, + {EntityType: msglib.EntityTypeGroups, SourceTable: "TD_WorkGroupAuth", JID: "project-room@conference", DisplayName: "Project Room", Confidence: 0.9}, + }} + session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { + RegisterISphereReadToolsWithDisplayEntities(server, fake, fakeDisplay) + }) + defer cleanup() + + callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveMessages, + Arguments: map[string]any{"query": "body", "limit": 5}, + }) + if err != nil { + t.Fatalf("call %s: %v", ToolNameReceiveMessages, err) + } + if callResult.IsError { + t.Fatalf("call result is error: %+v", callResult) + } + + var decoded struct { + Conversation *struct { + ConversationID string `json:"conversation_id"` + ConversationType string `json:"conversation_type"` + DisplayName string `json:"display_name"` + } `json:"conversation"` + Messages []struct { + SenderID string `json:"sender_id"` + SenderName string `json:"sender_name"` + Text string `json:"text"` + ContentText string `json:"content_text"` + } `json:"messages"` + } + payload, _ := json.Marshal(callResult.StructuredContent) + if err := json.Unmarshal(payload, &decoded); err != nil { + t.Fatalf("decode structured content %s: %v", payload, err) + } + if decoded.Conversation == nil || decoded.Conversation.DisplayName != "Project Room" || decoded.Conversation.ConversationType != "group" { + t.Fatalf("unexpected conversation: %+v", decoded.Conversation) + } + if len(decoded.Messages) != 1 { + t.Fatalf("messages = %+v, want one", decoded.Messages) + } + msg := decoded.Messages[0] + if msg.SenderID != "sender@example" || msg.SenderName != "Sender Name" { + t.Fatalf("unexpected sender fields: %+v", msg) + } + if msg.Text != "keep this body unchanged" || msg.ContentText != "keep this body unchanged" { + t.Fatalf("message text changed: %+v", msg) + } + if len(fakeDisplay.queries) != 2 { + t.Fatalf("display queries = %+v, want contacts and groups lookup", fakeDisplay.queries) + } + if fakeDisplay.queries[0].EntityType != msglib.EntityTypeContacts || fakeDisplay.queries[1].EntityType != msglib.EntityTypeGroups { + t.Fatalf("display query entity types = %+v", fakeDisplay.queries) + } +} + func TestISphereReceiveMessagesToolValidatesRemainingContractArgs(t *testing.T) { fake := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{