package tools import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" ) func TestISphereSendMessagePreviewPlansAndAuditsWithoutRawContent(t *testing.T) { audit := &fakeSendMessageAuditSink{} session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageTool(server, audit) }) defer cleanup() content := "hello preview" contentHash := sha256HexForSendMessageTest(content) callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSendMessage, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "content_text": content, "content_sha256": contentHash, "idempotency_key": "idem-preview-1", "execution_mode": "preview", }, }) if err != nil { t.Fatalf("call %s: %v", ToolNameSendMessage, err) } if callResult.IsError { t.Fatalf("call result is error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` ExecutionMode string `json:"execution_mode"` Connector string `json:"connector"` ConnectorStage string `json:"connector_stage"` Target map[string]any `json:"target"` Content map[string]any `json:"content"` SideEffects map[string]any `json:"side_effects"` 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 || decoded.SendStatus != "planned" || decoded.ExecutionMode != "preview" { t.Fatalf("unexpected preview status: %s", payload) } if decoded.Connector != "implatform-sidecar" || decoded.ConnectorStage != "preview" { t.Fatalf("unexpected connector fields: %s", payload) } if decoded.Target["target_type"] != "direct" || decoded.Target["target_id"] != "alice@imopenfire1-lanzhou" || decoded.Target["target_ref"] != "contact:alice@imopenfire1-lanzhou" { t.Fatalf("unexpected target: %#v", decoded.Target) } if decoded.Content["content_sha256"] != contentHash || decoded.Content["content_length"] != float64(len(content)) { t.Fatalf("unexpected content metadata: %#v", decoded.Content) } if _, hasRawBody := decoded.Content["content_text"]; hasRawBody { t.Fatalf("preview response leaked raw content body: %s", payload) } if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["clicked_ui"] != false || decoded.SideEffects["captured_network"] != false { t.Fatalf("side effect flags not all false: %#v", decoded.SideEffects) } if decoded.Audit["result"] != "planned" || decoded.Audit["tool"] != ToolNameSendMessage { t.Fatalf("unexpected audit response: %#v", decoded.Audit) } if len(audit.events) != 1 { t.Fatalf("audit events = %+v, want one", audit.events) } event := audit.events[0] if event.ContentSHA256 != contentHash || event.TargetRef != "contact:alice@imopenfire1-lanzhou" || event.Result != "planned" { t.Fatalf("unexpected audit event: %+v", event) } if event.ContentText != "" || event.IdempotencyKey != "" { t.Fatalf("audit event leaked raw content or idempotency key: %+v", event) } } func TestISphereSendMessageProductionIsBlockedWithoutSideEffects(t *testing.T) { audit := &fakeSendMessageAuditSink{} session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageTool(server, audit) }) defer cleanup() contentHash := sha256HexForSendMessageTest("blocked send") callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSendMessage, Arguments: map[string]any{ "target_type": "group", "target_id": "project-room@conference.imopenfire1-lanzhou", "content_text": "blocked send", "content_sha256": contentHash, "idempotency_key": "idem-production-1", "execution_mode": "production", }, }) if err != nil { t.Fatalf("call %s production: %v", ToolNameSendMessage, err) } if callResult.IsError { t.Fatalf("production blocked result should be structured, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` BlockedReason string `json:"blocked_reason"` Target map[string]any `json:"target"` SideEffects map[string]any `json:"side_effects"` Audit map[string]any `json:"audit"` } payload, _ := json.Marshal(callResult.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode structured content %s: %v", payload, err) } if decoded.OK || decoded.SendStatus != "blocked" || decoded.BlockedReason == "" { t.Fatalf("production was not clearly blocked: %s", payload) } if decoded.Target["target_ref"] != "group:project-room@conference.imopenfire1-lanzhou" { t.Fatalf("unexpected target: %#v", decoded.Target) } if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["uploaded_file"] != false || decoded.SideEffects["typed_text"] != false { t.Fatalf("side effect flags not all false: %#v", decoded.SideEffects) } if decoded.Audit["result"] != "blocked" { t.Fatalf("unexpected audit response: %#v", decoded.Audit) } if len(audit.events) != 1 || audit.events[0].Result != "blocked" { t.Fatalf("audit events = %+v, want one blocked event", audit.events) } } func TestISphereSendMessageRejectsContentHashMismatch(t *testing.T) { session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageTool(server, &fakeSendMessageAuditSink{}) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSendMessage, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "content_text": "hash mismatch", "content_sha256": sha256HexForSendMessageTest("different body"), "idempotency_key": "idem-hash-mismatch", "execution_mode": "preview", }, }) if err == nil && !callResult.IsError { t.Fatalf("hash mismatch was accepted") } } type fakeSendMessageAuditSink struct { events []SendMessageAuditEvent } func (f *fakeSendMessageAuditSink) AppendSendMessageAudit(_ context.Context, event SendMessageAuditEvent) error { f.events = append(f.events, event) return nil } func sha256HexForSendMessageTest(value string) string { sum := sha256.Sum256([]byte(value)) return hex.EncodeToString(sum[:]) }