package tools import ( "context" "encoding/json" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" ) func TestISphereReceiveFilesToolReturnsLogBackedFiles(t *testing.T) { fake := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ ID: "msg-file-1", ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou", Text: "redacted-report.docx", Timestamp: "1783423807000", Subject: "FILE_TRANSFER_CANCEL", }}}, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereFileTools(server, fake) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameReceiveFiles, Arguments: map[string]any{"mode": "list", "name_contains": "report", "limit": 5}, }) if err != nil { t.Fatalf("call %s: %v", ToolNameReceiveFiles, err) } if callResult.IsError { t.Fatalf("call result is error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` Mode string `json:"mode"` Files []struct { FileID string `json:"file_id"` MessageID string `json:"message_id"` ConversationID string `json:"conversation_id"` FileName string `json:"file_name"` SizeBytes *int64 `json:"size_bytes"` MimeType string `json:"mime_type"` CreatedAt string `json:"created_at"` DownloadRef *string `json:"download_ref"` SavedPath *string `json:"saved_path"` SHA256 *string `json:"sha256"` Source string `json:"source"` } `json:"files"` 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 || decoded.Mode != "list" { t.Fatalf("ok/mode = %v/%q in %s", decoded.OK, decoded.Mode, payload) } if len(decoded.Files) != 1 { t.Fatalf("files = %+v, want one", decoded.Files) } file := decoded.Files[0] if file.FileID != "msg-file-1:redacted-report.docx" || file.MessageID != "msg-file-1" || file.ConversationID != "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou" { t.Fatalf("unexpected file identity: %+v", file) } if file.FileName != "redacted-report.docx" || file.MimeType != "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || file.CreatedAt != "1783423807000" { t.Fatalf("unexpected file metadata: %+v", file) } if file.SizeBytes != nil || file.DownloadRef != nil || file.SavedPath != nil || file.SHA256 != nil { t.Fatalf("download/cache fields should be unknown: %+v", file) } if file.Source != "local_readonly" { t.Fatalf("source = %q", file.Source) } if decoded.Audit["tool"] != ToolNameReceiveFiles || decoded.Audit["result"] != "ok" || decoded.Audit["execution_mode"] != "read" { 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 TestISphereReceiveFilesToolValidatesContractArgs(t *testing.T) { fake := &fakeReceiveMessagesSource{ result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ ID: "msg-file-1", ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou", Text: "redacted-report.docx", Timestamp: "1783423807000", Subject: "FILE_TRANSFER_CANCEL", }}}, } session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereFileTools(server, fake) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameReceiveFiles, Arguments: map[string]any{ "mode": "list", "name_contains": "report", "source_preference": "local_readonly", "preview": true, "cursor": "", "output_dir": "", "limit": 5, }, }) 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: ToolNameReceiveFiles, Arguments: map[string]any{"mode": "list", "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: ToolNameReceiveFiles, Arguments: map[string]any{"mode": "list", "cursor": "next-page"}, }) if err == nil && !cursorResult.IsError { t.Fatalf("non-empty cursor was accepted before pagination exists") } outputResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameReceiveFiles, Arguments: map[string]any{"mode": "list", "output_dir": "C:\\tmp\\isphere"}, }) if err == nil && !outputResult.IsError { t.Fatalf("output_dir was accepted in list mode before download exists") } downloadResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameReceiveFiles, Arguments: map[string]any{ "mode": "download", "preview": true, "file_id": "msg-file-1:redacted-report.docx", }, }) if err != nil { t.Fatalf("download preview returned transport error: %v", err) } if downloadResult.IsError { t.Fatalf("download preview should return structured blocked content: %+v", downloadResult.Content) } var downloadPayload map[string]any downloadEncoded, _ := json.Marshal(downloadResult.StructuredContent) if err := json.Unmarshal(downloadEncoded, &downloadPayload); err != nil { t.Fatalf("decode download structured content %s: %v", downloadEncoded, err) } if downloadPayload["ok"] != false || downloadPayload["download_status"] != "blocked" || downloadPayload["blocked_reason_code"] != "file_cache_mapping_missing" { t.Fatalf("unexpected download preview block: %s", downloadEncoded) } } func TestISphereReceiveFilesDownloadPreviewBlockedWithoutMapping(t *testing.T) { fake := &fakeReceiveMessagesSource{} session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereFileTools(server, fake) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameReceiveFiles, Arguments: map[string]any{ "mode": "download", "preview": true, "file_ref": "msg-file-1:redacted-report.docx", "output_dir": "C:\\tmp\\isphere-downloads", }, }) if err != nil { t.Fatalf("download preview call returned transport error: %v", err) } if callResult.IsError { t.Fatalf("download preview should return structured blocked content, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` Mode string `json:"mode"` DownloadStatus string `json:"download_status"` BlockedReasonCode string `json:"blocked_reason_code"` FileContentsRead bool `json:"file_contents_read"` FileCopied bool `json:"file_copied"` RealDownload bool `json:"real_download_attempted"` } 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.Mode != "download" || decoded.DownloadStatus != "blocked" { t.Fatalf("unexpected blocked response: %+v payload=%s", decoded, payload) } if decoded.BlockedReasonCode != "file_cache_mapping_missing" { t.Fatalf("blocked reason = %q", decoded.BlockedReasonCode) } if decoded.FileContentsRead || decoded.FileCopied || decoded.RealDownload { t.Fatalf("download side effects must be false: %+v", decoded) } } func TestISphereReceiveFilesDownloadPreviewPlannedWithFixtureMapping(t *testing.T) { resolver := receiveFileDownloadResolverFunc(func(ctx context.Context, query ReceiveFileDownloadQuery) (ReceiveFileDownloadResolution, error) { if query.FileRef != "msg-file-1:redacted-report.docx" { t.Fatalf("resolver file_ref = %q", query.FileRef) } if query.OutputDir != "C:\\tmp\\isphere-downloads" { t.Fatalf("resolver output_dir = %q", query.OutputDir) } return ReceiveFileDownloadResolution{ Found: true, Source: "fixture-cache-mapping", MatchedScore: 100, }, nil }) session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { RegisterISphereFileToolsWithDownloadResolver(server, nil, resolver) }) defer cleanup() callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: ToolNameReceiveFiles, Arguments: map[string]any{ "mode": "download", "preview": true, "file_ref": "msg-file-1:redacted-report.docx", "output_dir": "C:\\tmp\\isphere-downloads", }, }) if err != nil { t.Fatalf("download preview call returned transport error: %v", err) } if callResult.IsError { t.Fatalf("download preview should return planned content, got error: %+v", callResult) } var decoded struct { OK bool `json:"ok"` Mode string `json:"mode"` DownloadStatus string `json:"download_status"` FileRef string `json:"file_ref"` MappingSource string `json:"mapping_source"` MatchedScore int `json:"matched_score"` FileContentsRead bool `json:"file_contents_read"` FileCopied bool `json:"file_copied"` RealDownload bool `json:"real_download_attempted"` } 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.Mode != "download" || decoded.DownloadStatus != "planned" { t.Fatalf("unexpected planned response: %+v payload=%s", decoded, payload) } if decoded.FileRef != "msg-file-1:redacted-report.docx" || decoded.MappingSource != "fixture-cache-mapping" || decoded.MatchedScore != 100 { t.Fatalf("unexpected mapping metadata: %+v", decoded) } if decoded.FileContentsRead || decoded.FileCopied || decoded.RealDownload { t.Fatalf("preview side effects must be false: %+v", decoded) } }