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", "file_id": "msg-file-1:redacted-report.docx"}, }) if err == nil && !downloadResult.IsError { t.Fatalf("download mode was accepted before cache mapping exists") } }