package tools import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" ) func TestISphereSendFilePreviewComputesHashAndSize(t *testing.T) { allowedDir := t.TempDir() filePath := filepath.Join(allowedDir, "hello.txt") if err := os.WriteFile(filePath, []byte("hello"), 0o600); err != nil { t.Fatalf("write fixture file: %v", err) } t.Setenv(EnvSendFileAllowedDir, allowedDir) session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendFileTool(server) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSendFile, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "file_path": filePath, "file_sha256": sha256HexForSendMessageTest("hello"), "idempotency_key": "file-preview-idem-1", "execution_mode": "preview", "preview": true, }, }) if err != nil { t.Fatalf("call %s: %v", ToolNameSendFile, err) } if callResult.IsError { t.Fatalf("preview should be structured success, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` FileSHA256 string `json:"file_sha256"` FileSizeBytes int64 `json:"file_size_bytes"` TargetRef string `json:"target_ref"` ProductionSendEnabled bool `json:"production_send_enabled"` RealSendAttempted bool `json:"real_send_attempted"` SideEffectFlags map[string]any `json:"side_effect_flags"` } payload, _ := json.Marshal(callResult.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode send-file preview payload %s: %v", payload, err) } if !decoded.OK || decoded.SendStatus != "planned" { t.Fatalf("preview status mismatch: %s", payload) } if decoded.FileSHA256 != sha256HexForSendMessageTest("hello") || decoded.FileSizeBytes != 5 { t.Fatalf("file metadata mismatch: %s", payload) } if decoded.TargetRef != "contact:alice@imopenfire1-lanzhou" { t.Fatalf("target_ref = %q, want contact ref", decoded.TargetRef) } if decoded.ProductionSendEnabled || decoded.RealSendAttempted { t.Fatalf("preview must not enable or attempt real send: %s", payload) } if decoded.SideEffectFlags["sent_file"] != false || decoded.SideEffectFlags["uploaded_file"] != false || decoded.SideEffectFlags["clicked_ui"] != false { t.Fatalf("side-effect flags mismatch: %#v", decoded.SideEffectFlags) } } func TestISphereSendFileRejectsPathOutsideAllowedDir(t *testing.T) { allowedDir := t.TempDir() outsideDir := t.TempDir() filePath := filepath.Join(outsideDir, "outside.txt") if err := os.WriteFile(filePath, []byte("outside"), 0o600); err != nil { t.Fatalf("write outside fixture file: %v", err) } t.Setenv(EnvSendFileAllowedDir, allowedDir) session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendFileTool(server) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSendFile, Arguments: map[string]any{ "target_type": "group", "target_id": "project-room@conference.imopenfire1-lanzhou", "file_path": filePath, "file_sha256": sha256HexForSendMessageTest("outside"), "idempotency_key": "file-outside-idem-1", "execution_mode": "preview", "preview": true, }, }) if err != nil { t.Fatalf("call %s: %v", ToolNameSendFile, err) } if callResult.IsError { t.Fatalf("outside path should be structured blocked metadata, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` BlockedReason string `json:"blocked_reason"` TargetRef string `json:"target_ref"` SideEffectFlags map[string]any `json:"side_effect_flags"` } payload, _ := json.Marshal(callResult.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode outside-path payload %s: %v", payload, err) } if decoded.OK || decoded.SendStatus != "blocked" { t.Fatalf("outside path should be blocked: %s", payload) } if !strings.Contains(decoded.BlockedReason, EnvSendFileAllowedDir) { t.Fatalf("blocked reason should mention allowed-dir env: %q", decoded.BlockedReason) } if decoded.TargetRef != "group:project-room@conference.imopenfire1-lanzhou" { t.Fatalf("target_ref = %q, want group ref", decoded.TargetRef) } if decoded.SideEffectFlags["sent_file"] != false || decoded.SideEffectFlags["uploaded_file"] != false || decoded.SideEffectFlags["typed_text"] != false { t.Fatalf("blocked path should have no side effects: %#v", decoded.SideEffectFlags) } } func TestISphereSendFileDuplicateIdempotencyDetected(t *testing.T) { allowedDir := t.TempDir() filePath := writeSendFileFixture(t, allowedDir, "duplicate.txt", "duplicate") t.Setenv(EnvSendFileAllowedDir, allowedDir) t.Setenv(EnvSendFileIdempotencyPath, filepath.Join(t.TempDir(), "send-file-idempotency.jsonl")) t.Setenv(EnvSendFileAuditPath, filepath.Join(t.TempDir(), "send-file-audit.jsonl")) session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendFileTool(server) }) defer cleanup() args := map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "file_path": filePath, "file_sha256": sha256HexForSendMessageTest("duplicate"), "idempotency_key": "file-duplicate-idem-1", "execution_mode": "preview", "preview": true, } if _, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendFile, Arguments: args}); err != nil { t.Fatalf("first send-file preview: %v", err) } second, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendFile, Arguments: args}) if err != nil { t.Fatalf("second send-file preview: %v", err) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` RealSendAttempted bool `json:"real_send_attempted"` Idempotency map[string]any `json:"idempotency"` SideEffectFlags map[string]any `json:"side_effect_flags"` } 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" || decoded.RealSendAttempted { t.Fatalf("duplicate should remain planned preview/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.SideEffectFlags["sent_file"] != false || decoded.SideEffectFlags["uploaded_file"] != false { t.Fatalf("duplicate should have no file side effects: %#v", decoded.SideEffectFlags) } } func TestISphereSendFileIdempotencyConflictBlocked(t *testing.T) { allowedDir := t.TempDir() firstPath := writeSendFileFixture(t, allowedDir, "first.txt", "first") secondPath := writeSendFileFixture(t, allowedDir, "second.txt", "second") t.Setenv(EnvSendFileAllowedDir, allowedDir) t.Setenv(EnvSendFileIdempotencyPath, filepath.Join(t.TempDir(), "send-file-idempotency.jsonl")) t.Setenv(EnvSendFileAuditPath, filepath.Join(t.TempDir(), "send-file-audit.jsonl")) session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendFileTool(server) }) defer cleanup() sharedKey := "file-conflict-idem-1" _, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendFile, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "file_path": firstPath, "file_sha256": sha256HexForSendMessageTest("first"), "idempotency_key": sharedKey, "execution_mode": "preview", }}) if err != nil { t.Fatalf("first send-file preview: %v", err) } second, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendFile, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "file_path": secondPath, "file_sha256": sha256HexForSendMessageTest("second"), "idempotency_key": sharedKey, "execution_mode": "preview", }}) if err != nil { t.Fatalf("conflict send-file preview: %v", err) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` BlockedReason string `json:"blocked_reason"` RealSendAttempted bool `json:"real_send_attempted"` Idempotency map[string]any `json:"idempotency"` SideEffectFlags map[string]any `json:"side_effect_flags"` } 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.BlockedReason == "" || decoded.RealSendAttempted { t.Fatalf("conflict should be blocked/no-send: %s", payload) } if decoded.Idempotency["duplicate_detected"] != false || decoded.Idempotency["conflict_detected"] != true { t.Fatalf("conflict idempotency fields mismatch: %#v", decoded.Idempotency) } if decoded.SideEffectFlags["sent_file"] != false || decoded.SideEffectFlags["uploaded_file"] != false || decoded.SideEffectFlags["typed_text"] != false { t.Fatalf("conflict should have no side effects: %#v", decoded.SideEffectFlags) } } func TestISphereSendFileAuditRedactionIsStable(t *testing.T) { allowedDir := t.TempDir() filePath := writeSendFileFixture(t, allowedDir, "secret.txt", "secret file bytes") auditPath := filepath.Join(t.TempDir(), "send-file-audit.jsonl") t.Setenv(EnvSendFileAllowedDir, allowedDir) t.Setenv(EnvSendFileAuditPath, auditPath) t.Setenv(EnvSendFileIdempotencyPath, filepath.Join(t.TempDir(), "send-file-idempotency.jsonl")) session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendFileTool(server) }) defer cleanup() fileHash := sha256HexForSendMessageTest("secret file bytes") idempotencyKey := "raw-file-idempotency-key-must-not-be-stored" callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: ToolNameSendFile, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "file_path": filePath, "file_sha256": fileHash, "idempotency_key": idempotencyKey, "execution_mode": "preview", }}) if err != nil { t.Fatalf("send-file preview: %v", err) } if callResult.IsError { t.Fatalf("send-file preview should not be tool error: %+v", callResult) } payload, err := os.ReadFile(auditPath) if err != nil { t.Fatalf("read send-file audit: %v", err) } audit := string(payload) for _, forbidden := range []string{filePath, idempotencyKey, "secret file bytes"} { if strings.Contains(audit, forbidden) { t.Fatalf("send-file audit leaked forbidden value %q: %s", forbidden, audit) } } if !strings.Contains(audit, fileHash) { t.Fatalf("send-file audit missing file hash: %s", audit) } } func TestISphereSendFileProductionBlockedWithoutSideEffects(t *testing.T) { allowedDir := t.TempDir() filePath := filepath.Join(allowedDir, "blocked.txt") if err := os.WriteFile(filePath, []byte("blocked"), 0o600); err != nil { t.Fatalf("write blocked fixture file: %v", err) } t.Setenv(EnvSendFileAllowedDir, allowedDir) session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereSendFileTool(server) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameSendFile, Arguments: map[string]any{ "target_type": "direct", "target_id": "alice@imopenfire1-lanzhou", "file_path": filePath, "file_sha256": sha256HexForSendMessageTest("blocked"), "idempotency_key": "file-production-idem-1", "execution_mode": "production", }, }) if err != nil { t.Fatalf("call %s production: %v", ToolNameSendFile, err) } if callResult.IsError { t.Fatalf("production should be structured blocked metadata, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` SendStatus string `json:"send_status"` BlockedReason string `json:"blocked_reason"` ProductionSendEnabled bool `json:"production_send_enabled"` RealSendAttempted bool `json:"real_send_attempted"` SideEffectFlags map[string]any `json:"side_effect_flags"` } payload, _ := json.Marshal(callResult.StructuredContent) if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode production-blocked payload %s: %v", payload, err) } if decoded.OK || decoded.SendStatus != "blocked" || decoded.BlockedReason == "" { t.Fatalf("production should be blocked with reason: %s", payload) } if decoded.ProductionSendEnabled || decoded.RealSendAttempted { t.Fatalf("production must not be enabled or attempted: %s", payload) } if decoded.SideEffectFlags["sent_file"] != false || decoded.SideEffectFlags["uploaded_file"] != false || decoded.SideEffectFlags["captured_network"] != false { t.Fatalf("production blocked should have no side effects: %#v", decoded.SideEffectFlags) } } func writeSendFileFixture(t *testing.T, dir string, name string, body string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatalf("write send-file fixture %s: %v", name, err) } return path }