package tools import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "os" "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" ) func TestISphereSendMessagePreviewPlansAndAuditsWithoutRawContent(t *testing.T) { audit := &fakeSendMessageAuditSink{} idempotency := newFakeSendMessageIdempotencyStore() session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithState(server, audit, idempotency) }) 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{} idempotency := newFakeSendMessageIdempotencyStore() session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithState(server, audit, idempotency) }) 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 TestISphereSendMessageProductionBlockedIncludesGateReason(t *testing.T) { audit := &fakeSendMessageAuditSink{} session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithState(server, audit, newFakeSendMessageIdempotencyStore()) }) defer cleanup() content := "blocked gate reason" 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": sha256HexForSendMessageTest(content), "idempotency_key": "idem-gate-reason-1", "execution_mode": "production", }, }) if err != nil { t.Fatalf("call production blocked gate reason: %v", err) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` GateReasonCode string `json:"gate_reason_code"` GateReasonMessage string `json:"gate_reason_message"` } payload, _ := json.Marshal(callResult.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode payload %s: %v", payload, err) } if decoded.OK || decoded.SendStatus != "blocked" || decoded.GateReasonCode != "send_disabled_by_default" || decoded.GateReasonMessage == "" { t.Fatalf("production blocked response missing gate reason: %s", payload) } if len(audit.events) != 1 || audit.events[0].GateReasonCode != "send_disabled_by_default" || audit.events[0].GateReasonMessage == "" { t.Fatalf("audit event missing gate reason: %+v", audit.events) } } func TestISphereSendMessageFakeConnectorAcceptedAudit(t *testing.T) { audit := &fakeSendMessageAuditSink{} idempotency := newFakeSendMessageIdempotencyStore() connector := &fakeSendMessageConnector{ result: SendMessageConnectorResult{ Accepted: true, Status: "accepted", AckRef: "fake-ack-1", ConnectorMode: "fake", }, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithStateAndConnector(server, audit, idempotency, connector) }) defer cleanup() content := "fake connector accepted" 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-fake-accepted-1", "execution_mode": "production", }, }) if err != nil { t.Fatalf("call %s fake connector accepted: %v", ToolNameSendMessage, err) } if callResult.IsError { t.Fatalf("fake connector accepted result should be structured, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` ConnectorMode string `json:"connector_mode"` ProductionSendEnabled bool `json:"production_send_enabled"` 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 fake accepted payload %s: %v", payload, err) } if !decoded.OK || decoded.SendStatus != "accepted" || decoded.ConnectorMode != "fake" { t.Fatalf("unexpected fake accepted response: %s", payload) } if decoded.ProductionSendEnabled { t.Fatalf("fake connector must not flip default production_send_enabled: %s", payload) } if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["clicked_ui"] != false || decoded.SideEffects["captured_network"] != false { t.Fatalf("fake connector should not report real side effects: %#v", decoded.SideEffects) } if decoded.Audit["ack_ref"] != "fake-ack-1" || decoded.Audit["result"] != "accepted" { t.Fatalf("unexpected fake accepted audit: %#v", decoded.Audit) } if strings.Contains(string(payload), content) || strings.Contains(string(payload), "idem-fake-accepted-1") { t.Fatalf("fake accepted payload leaked raw content or idempotency key: %s", payload) } if connector.calls != 1 { t.Fatalf("fake connector calls = %d, want 1", connector.calls) } if len(audit.events) != 1 { t.Fatalf("audit events = %+v, want one", audit.events) } event := audit.events[0] if event.Result != "accepted" || event.AckRef != "fake-ack-1" || event.ConnectorMode != "fake" { 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 TestISphereSendMessageFakeConnectorFailureAudit(t *testing.T) { audit := &fakeSendMessageAuditSink{} idempotency := newFakeSendMessageIdempotencyStore() connector := &fakeSendMessageConnector{ result: SendMessageConnectorResult{ Accepted: false, Status: "failed", ErrorCode: "fake_rejected", ErrorMessage: "fake connector rejected the request", ConnectorMode: "fake", }, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithStateAndConnector(server, audit, idempotency, connector) }) defer cleanup() content := "fake connector failed" contentHash := sha256HexForSendMessageTest(content) 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": content, "content_sha256": contentHash, "idempotency_key": "idem-fake-failed-1", "execution_mode": "production", }, }) if err != nil { t.Fatalf("call %s fake connector failure: %v", ToolNameSendMessage, err) } if callResult.IsError { t.Fatalf("fake connector failure should be structured, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` ErrorCode string `json:"error_code"` 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 fake failure payload %s: %v", payload, err) } if decoded.OK || decoded.SendStatus != "failed" || decoded.ErrorCode != "fake_rejected" { t.Fatalf("unexpected fake failure response: %s", payload) } if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["typed_text"] != false { t.Fatalf("fake failure should not report real side effects: %#v", decoded.SideEffects) } if decoded.Audit["error_code"] != "fake_rejected" || decoded.Audit["result"] != "failed" { t.Fatalf("unexpected fake failure audit: %#v", decoded.Audit) } if strings.Contains(string(payload), content) || strings.Contains(string(payload), "idem-fake-failed-1") { t.Fatalf("fake failure payload leaked raw content or idempotency key: %s", payload) } if len(audit.events) != 1 { t.Fatalf("audit events = %+v, want one", audit.events) } event := audit.events[0] if event.Result != "failed" || event.ErrorCode != "fake_rejected" || event.ConnectorMode != "fake" { t.Fatalf("unexpected audit event: %+v", event) } } func TestISphereSendMessageRejectsContentHashMismatch(t *testing.T) { session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithState(server, &fakeSendMessageAuditSink{}, newFakeSendMessageIdempotencyStore()) }) 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") } } func TestISphereSendMessageAuditRedactionIsStable(t *testing.T) { auditPath := t.TempDir() + "\\send-audit.jsonl" audit := fileSendMessageAuditSink{path: auditPath} session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithState(server, audit, newFakeSendMessageIdempotencyStore()) }) defer cleanup() content := "raw body must not be stored in audit" idempotencyKey := "raw-idempotency-key-must-not-be-stored" 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": idempotencyKey, "execution_mode": "preview", }, }) if err != nil { t.Fatalf("call %s: %v", ToolNameSendMessage, err) } if callResult.IsError { t.Fatalf("preview should not return error: %+v", callResult) } payload, err := os.ReadFile(auditPath) if err != nil { t.Fatalf("read audit file: %v", err) } if strings.Contains(string(payload), content) || strings.Contains(string(payload), idempotencyKey) { t.Fatalf("audit file leaked raw content or idempotency key: %s", payload) } if !strings.Contains(string(payload), contentHash) { t.Fatalf("audit file missing content hash: %s", payload) } } func TestISphereSendMessageDetectsDuplicateIdempotencyKey(t *testing.T) { audit := &fakeSendMessageAuditSink{} idempotency := newFakeSendMessageIdempotencyStore() session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithState(server, audit, idempotency) }) defer cleanup() content := "duplicate preview" contentHash := sha256HexForSendMessageTest(content) args := map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "content_text": content, "content_sha256": contentHash, "idempotency_key": "idem-duplicate-1", "execution_mode": "preview", } if _, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendMessage, Arguments: args}); err != nil { t.Fatalf("first call: %v", err) } second, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendMessage, Arguments: args}) if err != nil { t.Fatalf("second call: %v", err) } if second.IsError { t.Fatalf("duplicate should be structured preview metadata, got error: %+v", second) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` Idempotency map[string]any `json:"idempotency"` SideEffects map[string]any `json:"side_effects"` } payload, _ := json.Marshal(second.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode duplicate payload %s: %v", payload, err) } if !decoded.OK || decoded.SendStatus != "planned" { t.Fatalf("duplicate preview should remain planned/no-send: %s", payload) } if decoded.Idempotency["duplicate_detected"] != true || decoded.Idempotency["conflict_detected"] != false { t.Fatalf("duplicate idempotency fields mismatch: %#v", decoded.Idempotency) } if decoded.Idempotency["first_audit_ref"] == "" { t.Fatalf("duplicate response missing first_audit_ref: %#v", decoded.Idempotency) } if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["clicked_ui"] != false { t.Fatalf("duplicate preview should not have side effects: %#v", decoded.SideEffects) } if len(audit.events) != 2 || audit.events[1].Result != "planned" { t.Fatalf("audit events = %+v, want two planned events", audit.events) } } func TestISphereSendMessageBlocksIdempotencyKeyConflict(t *testing.T) { audit := &fakeSendMessageAuditSink{} idempotency := newFakeSendMessageIdempotencyStore() session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendMessageToolWithState(server, audit, idempotency) }) defer cleanup() firstContent := "first body" firstHash := sha256HexForSendMessageTest(firstContent) _, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendMessage, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "content_text": firstContent, "content_sha256": firstHash, "idempotency_key": "idem-conflict-1", "execution_mode": "preview", }}) if err != nil { t.Fatalf("first call: %v", err) } secondContent := "different body" secondHash := sha256HexForSendMessageTest(secondContent) second, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendMessage, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "content_text": secondContent, "content_sha256": secondHash, "idempotency_key": "idem-conflict-1", "execution_mode": "preview", }}) if err != nil { t.Fatalf("conflict call: %v", err) } if second.IsError { t.Fatalf("idempotency conflict should be structured blocked metadata, got error: %+v", second) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` BlockedReason string `json:"blocked_reason"` ConnectorStage string `json:"connector_stage"` Idempotency map[string]any `json:"idempotency"` SideEffects map[string]any `json:"side_effects"` } payload, _ := json.Marshal(second.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode conflict payload %s: %v", payload, err) } if decoded.OK || decoded.SendStatus != "blocked" || decoded.ConnectorStage != "blocked" { t.Fatalf("conflict should be blocked: %s", payload) } if !strings.Contains(strings.ToLower(decoded.BlockedReason), "idempotency") { t.Fatalf("blocked reason should explain idempotency conflict: %q", decoded.BlockedReason) } if decoded.Idempotency["duplicate_detected"] != false || decoded.Idempotency["conflict_detected"] != true { t.Fatalf("conflict idempotency fields mismatch: %#v", decoded.Idempotency) } if decoded.SideEffects["sent_message"] != false || decoded.SideEffects["typed_text"] != false { t.Fatalf("conflict should not have side effects: %#v", decoded.SideEffects) } if len(audit.events) != 2 || audit.events[1].Result != "blocked" { t.Fatalf("audit events = %+v, want second blocked event", audit.events) } } func TestFileSendMessageIdempotencyStoreDetectsDuplicateAndConflict(t *testing.T) { store := fileSendMessageIdempotencyStore{path: t.TempDir() + "\\send-idempotency.jsonl"} first := SendMessageIdempotencyRecord{ Tool: ToolNameSendMessage, TargetRef: "contact:alice@imopenfire1-lanzhou", ContentSHA256: sha256HexForSendMessageTest("first body"), IdempotencyKeySHA256: sha256HexForSendMessageTest("idem-file-store"), AuditRef: "send-message:first", StartedAt: "2026-07-10T00:00:00Z", } firstDecision, err := store.ReserveSendMessageIdempotency(context.Background(), first) if err != nil { t.Fatalf("reserve first: %v", err) } if firstDecision.Duplicate || firstDecision.Conflict { t.Fatalf("first decision = %+v, want fresh", firstDecision) } duplicateDecision, err := store.ReserveSendMessageIdempotency(context.Background(), first) if err != nil { t.Fatalf("reserve duplicate: %v", err) } if !duplicateDecision.Duplicate || duplicateDecision.Conflict || duplicateDecision.FirstAuditRef != first.AuditRef { t.Fatalf("duplicate decision = %+v", duplicateDecision) } conflict := first conflict.ContentSHA256 = sha256HexForSendMessageTest("changed body") conflict.AuditRef = "send-message:conflict" conflictDecision, err := store.ReserveSendMessageIdempotency(context.Background(), conflict) if err != nil { t.Fatalf("reserve conflict: %v", err) } if !conflictDecision.Conflict || conflictDecision.Duplicate { t.Fatalf("conflict decision = %+v", conflictDecision) } if conflictDecision.FirstAuditRef != first.AuditRef { t.Fatalf("conflict first audit ref = %q, want %q", conflictDecision.FirstAuditRef, first.AuditRef) } } type fakeSendMessageAuditSink struct { events []SendMessageAuditEvent } func (f *fakeSendMessageAuditSink) AppendSendMessageAudit(_ context.Context, event SendMessageAuditEvent) error { f.events = append(f.events, event) return nil } type fakeSendMessageIdempotencyStore struct { records []SendMessageIdempotencyRecord } func newFakeSendMessageIdempotencyStore() *fakeSendMessageIdempotencyStore { return &fakeSendMessageIdempotencyStore{} } func (f *fakeSendMessageIdempotencyStore) ReserveSendMessageIdempotency(_ context.Context, record SendMessageIdempotencyRecord) (SendMessageIdempotencyDecision, error) { for _, existing := range f.records { if existing.IdempotencyKeySHA256 != record.IdempotencyKeySHA256 { continue } decision := SendMessageIdempotencyDecision{ FirstAuditRef: existing.AuditRef, FirstStartedAt: existing.StartedAt, } if existing.TargetRef == record.TargetRef && existing.ContentSHA256 == record.ContentSHA256 { decision.Duplicate = true return decision, nil } decision.Conflict = true return decision, nil } f.records = append(f.records, record) return SendMessageIdempotencyDecision{}, nil } type fakeSendMessageConnector struct { result SendMessageConnectorResult err error calls int } func (f *fakeSendMessageConnector) ExecuteSendMessage(_ context.Context, _ SendMessageConnectorRequest) (SendMessageConnectorResult, error) { f.calls++ return f.result, f.err } func sha256HexForSendMessageTest(value string) string { sum := sha256.Sum256([]byte(value)) return hex.EncodeToString(sum[:]) }