Skip to main content
Glama

Slack MCP

MIT License
696
  • Apple
  • Linux
conversations_test.go14.6 kB
package handler import ( "context" "encoding/csv" "fmt" "os" "regexp" "strconv" "strings" "testing" "time" "github.com/google/uuid" "github.com/korotovsky/slack-mcp-server/pkg/test/util" "github.com/openai/openai-go" "github.com/openai/openai-go/option" "github.com/openai/openai-go/packages/param" "github.com/openai/openai-go/responses" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestIntegrationConversations(t *testing.T) { sseKey := uuid.New().String() require.NotEmpty(t, sseKey, "sseKey must be generated for integration tests") apiKey := os.Getenv("SLACK_MCP_OPENAI_API") require.NotEmpty(t, apiKey, "SLACK_MCP_OPENAI_API must be set for integration tests") cfg := util.MCPConfig{ SSEKey: sseKey, MessageToolEnabled: true, MessageToolMark: true, } mcp, err := util.SetupMCP(cfg) if err != nil { t.Fatalf("Failed to set up MCP server: %v", err) } fwd, err := util.SetupForwarding(context.Background(), "http://"+mcp.Host+":"+strconv.Itoa(mcp.Port)) if err != nil { t.Fatalf("Failed to set up ngrok forwarding: %v", err) } defer fwd.Shutdown() defer mcp.Shutdown() client := openai.NewClient(option.WithAPIKey(apiKey)) ctx := context.Background() type matchingRule struct { csvFieldName string csvFieldValueRE string RowPosition *int TotalRows *int } type tc struct { name string input string expectedToolName string expectedToolOutputMatchingRules []matchingRule expectedLLMOutputMatchingRules []string } cases := []tc{ { name: "Test conversations_history tool", input: "Provide a list of slack messages from #testcase-1", expectedToolName: "conversations_history", expectedToolOutputMatchingRules: []matchingRule{ { csvFieldName: "Text", csvFieldValueRE: "^message 3$", }, { csvFieldName: "Text", csvFieldValueRE: "^message 2$", }, { csvFieldName: "Text", csvFieldValueRE: "^message 1$", }, }, expectedLLMOutputMatchingRules: []string{ "message 1", "message 2", "message 3", }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { params := responses.ResponseNewParams{ Model: "gpt-4.1-mini", Tools: []responses.ToolUnionParam{ { OfMcp: &responses.ToolMcpParam{ ServerLabel: "slack-mcp-server", ServerURL: fmt.Sprintf("%s://%s/sse", fwd.URL.Scheme, fwd.URL.Host), RequireApproval: responses.ToolMcpRequireApprovalUnionParam{ OfMcpToolApprovalSetting: param.NewOpt("never"), }, Headers: map[string]string{ "Authorization": "Bearer " + sseKey, }, }, }, }, Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String(tc.input), }, } resp, err := client.Responses.New(ctx, params) require.NoError(t, err, "API call failed") assert.NotNil(t, resp.Status, "completed") var llmOutput strings.Builder var toolOutput strings.Builder for _, out := range resp.Output { if out.Type == "message" && out.Role == "assistant" { for _, c := range out.Content { if c.Type == "output_text" { llmOutput.WriteString(c.Text) } } } if out.Type == "mcp_call" && out.Name == tc.expectedToolName { toolOutput.WriteString(out.Output) } } require.NotEmpty(t, toolOutput, "no tool output captured") // Parse CSV reader := csv.NewReader(strings.NewReader(toolOutput.String())) rows, err := reader.ReadAll() require.NoError(t, err, "failed to parse CSV") header := rows[0] dataRows := rows[1:] colIndex := map[string]int{} for i, col := range header { colIndex[col] = i } for _, rule := range tc.expectedToolOutputMatchingRules { if rule.TotalRows != nil && *rule.TotalRows > 0 { assert.Equalf(t, *rule.TotalRows, len(dataRows), "expected %d data rows, got %d", rule.TotalRows, len(dataRows)) } idx, ok := colIndex[rule.csvFieldName] require.Truef(t, ok, "CSV did not contain column %q, toolOutput: %q", rule.csvFieldName, toolOutput.String()) re, err := regexp.Compile(rule.csvFieldValueRE) require.NoErrorf(t, err, "invalid regex %q", rule.csvFieldValueRE) if rule.RowPosition != nil && *rule.RowPosition >= 0 { require.Lessf(t, rule.RowPosition, len(dataRows), "RowPosition %d out of range (only %d data rows)", rule.RowPosition, len(dataRows)) value := dataRows[*rule.RowPosition][idx] assert.Regexpf(t, re, value, "row %d, column %q: expected to match %q, got %q", rule.RowPosition, rule.csvFieldName, rule.csvFieldValueRE, value) continue } found := false for _, row := range dataRows { if idx < len(row) && re.MatchString(row[idx]) { found = true break } } assert.Truef(t, found, "no row in column %q matched %q; full CSV:\n%s", rule.csvFieldName, rule.csvFieldValueRE, toolOutput.String()) } for _, pattern := range tc.expectedLLMOutputMatchingRules { re, err := regexp.Compile(pattern) require.NoErrorf(t, err, "invalid LLM regex %q", pattern) assert.Regexpf(t, re, llmOutput.String(), "LLM output did not match regex %q; output:\n%s", pattern, llmOutput.String()) } }) } } func TestUnitParseFlexibleDate(t *testing.T) { tests := []struct { name string input string wantDate string wantErr bool }{ // Standard formats (existing) { name: "YYYY-MM-DD", input: "2025-07-15", wantDate: "2025-07-15", wantErr: false, }, { name: "YYYY/MM/DD", input: "2025/07/15", wantDate: "2025-07-15", wantErr: false, }, // New flexible month-year formats { name: "Month Year - July 2025", input: "July 2025", wantDate: "2025-07-01", wantErr: false, }, { name: "Year Month - 2025 July", input: "2025 July", wantDate: "2025-07-01", wantErr: false, }, { name: "Abbreviated Month Year - Jul 2025", input: "Jul 2025", wantDate: "2025-07-01", wantErr: false, }, { name: "Year Abbreviated Month - 2025 Jul", input: "2025 Jul", wantDate: "2025-07-01", wantErr: false, }, { name: "Case insensitive - july 2025", input: "july 2025", wantDate: "2025-07-01", wantErr: false, }, { name: "Case insensitive - JULY 2025", input: "JULY 2025", wantDate: "2025-07-01", wantErr: false, }, // Day-Month-Year formats { name: "1-July-2025", input: "1-July-2025", wantDate: "2025-07-01", wantErr: false, }, { name: "July-25-2025", input: "July-25-2025", wantDate: "2025-07-25", wantErr: false, }, { name: "July 10 2025", input: "July 10 2025", wantDate: "2025-07-10", wantErr: false, }, { name: "10 July 2025", input: "10 July 2025", wantDate: "2025-07-10", wantErr: false, }, { name: "31-December-2025", input: "31-December-2025", wantDate: "2025-12-31", wantErr: false, }, { name: "2025 July 10", input: "2025 July 10", wantDate: "2025-07-10", wantErr: false, }, // Various month names { name: "January full name", input: "January 2025", wantDate: "2025-01-01", wantErr: false, }, { name: "February abbreviated", input: "Feb 2025", wantDate: "2025-02-01", wantErr: false, }, { name: "September with Sept abbreviation", input: "Sept 2025", wantDate: "2025-09-01", wantErr: false, }, // Relative dates { name: "today", input: "today", wantDate: time.Now().UTC().Format("2006-01-02"), wantErr: false, }, { name: "yesterday", input: "yesterday", wantDate: time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02"), wantErr: false, }, { name: "Today with capital T", input: "Today", wantDate: time.Now().UTC().Format("2006-01-02"), wantErr: false, }, { name: "Yesterday with capital Y", input: "Yesterday", wantDate: time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02"), wantErr: false, }, { name: "TODAY all caps", input: "TODAY", wantDate: time.Now().UTC().Format("2006-01-02"), wantErr: false, }, { name: "YESTERDAY all caps", input: "YESTERDAY", wantDate: time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02"), wantErr: false, }, { name: "tomorrow", input: "tomorrow", wantDate: time.Now().UTC().AddDate(0, 0, 1).Format("2006-01-02"), wantErr: false, }, { name: "5 days ago", input: "5 days ago", wantDate: time.Now().UTC().AddDate(0, 0, -5).Format("2006-01-02"), wantErr: false, }, { name: "1 day ago", input: "1 day ago", wantDate: time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02"), wantErr: false, }, // Edge cases { name: "Whitespace trimming", input: " July 2025 ", wantDate: "2025-07-01", wantErr: false, }, { name: "Invalid month name", input: "Jully 2025", wantDate: "", wantErr: true, }, { name: "Invalid date format", input: "2025-13-01", wantDate: "", wantErr: true, }, { name: "Invalid day for month", input: "31-February-2025", wantDate: "", wantErr: true, }, { name: "Empty string", input: "", wantDate: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, gotDate, err := parseFlexibleDate(tt.input) if (err != nil) != tt.wantErr { t.Errorf("parseFlexibleDate() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && gotDate != tt.wantDate { t.Errorf("parseFlexibleDate() gotDate = %v, want %v", gotDate, tt.wantDate) } }) } } func TestUnitBuildDateFiltersUnit(t *testing.T) { tests := []struct { name string before string after string on string during string want map[string]string wantErr bool }{ { name: "On with flexible format July 2025", before: "", after: "", on: "July 2025", during: "", want: map[string]string{"on": "2025-07-01"}, wantErr: false, }, { name: "Before and After with flexible formats", before: "December 2025", after: "January 2025", on: "", during: "", want: map[string]string{"before": "2025-12-01", "after": "2025-01-01"}, wantErr: false, }, { name: "During with day format", before: "", after: "", on: "", during: "15-July-2025", want: map[string]string{"during": "2025-07-15"}, wantErr: false, }, { name: "Error: on with other filters", before: "2025-12-01", after: "", on: "July 2025", during: "", want: nil, wantErr: true, }, { name: "Error: during with before", before: "2025-12-01", after: "", on: "", during: "July 2025", want: nil, wantErr: true, }, { name: "Error: after date is after before date", before: "January 2025", after: "December 2025", on: "", during: "", want: nil, wantErr: true, }, { name: "Valid: complex date formats", before: "31-December-2025", after: "1-January-2025", on: "", during: "", want: map[string]string{"before": "2025-12-31", "after": "2025-01-01"}, wantErr: false, }, { name: "Error: invalid date format", before: "", after: "", on: "Jully 2025", during: "", want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := buildDateFilters(tt.before, tt.after, tt.on, tt.during) if (err != nil) != tt.wantErr { t.Errorf("buildDateFilters() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr { if len(got) != len(tt.want) { t.Errorf("buildDateFilters() got map length = %v, want %v", len(got), len(tt.want)) return } for k, v := range tt.want { if got[k] != v { t.Errorf("buildDateFilters() got[%s] = %v, want %v", k, got[k], v) } } } }) } } func TestUnitLimitByExpression_Valid(t *testing.T) { now := time.Now() oneMonthAgo := now.AddDate(0, -1, 0) twoMonthsAgo := now.AddDate(0, -2, 0) oneMonthSpan := int64(now.Sub(oneMonthAgo).Seconds()) twoMonthSpan := int64(now.Sub(twoMonthsAgo).Seconds()) const tolerance = 86400 tests := []struct { name string input string minSecs int64 // inclusive maxSecs int64 // exclusive }{ {"1 day", "", 0, 86400}, // default case with no input test {"1 day", "1d", 0, 86400}, {"2 days", "2d", 86400, 172800}, {"1 week", "1w", 6 * 86400, 7 * 86400}, {"2 weeks", "2w", 13 * 86400, 14 * 86400}, {"1 month", "1m", oneMonthSpan - tolerance, oneMonthSpan + tolerance}, {"2 months", "2m", twoMonthSpan - tolerance, twoMonthSpan + tolerance}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { slackLimit, oldestStr, latestStr, err := limitByExpression(tt.input, defaultConversationsExpressionLimit) if err != nil { t.Fatalf("expected no error for %q, got %v", tt.input, err) } if slackLimit != 100 { t.Errorf("expected slackLimit=100 for %q, got %d", tt.input, slackLimit) } // Parse the "1234567890.000000" format back to an integer o, err := strconv.ParseInt(strings.TrimSuffix(oldestStr, ".000000"), 10, 64) if err != nil { t.Fatalf("invalid oldest timestamp %q: %v", oldestStr, err) } l, err := strconv.ParseInt(strings.TrimSuffix(latestStr, ".000000"), 10, 64) if err != nil { t.Fatalf("invalid latest timestamp %q: %v", latestStr, err) } if l <= o { t.Errorf("for %q expected latest(%d) > oldest(%d)", tt.input, l, o) } diff := l - o if diff < tt.minSecs || diff >= tt.maxSecs { t.Errorf( "for %q expected span in [%d, %d), got %d", tt.input, tt.minSecs, tt.maxSecs, diff, ) } }) } } func TestUnitLimitByExpression_Invalid(t *testing.T) { invalid := []string{ "d", // too short "0d", // zero "-1d", // negative "1x", // bad suffix "1", // missing suffix "01", // no suffix + zero value } for _, input := range invalid { t.Run(input, func(t *testing.T) { _, _, _, err := limitByExpression(input, defaultConversationsExpressionLimit) if err == nil { t.Errorf("expected error for %q, got nil", input) } }) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/korotovsky/slack-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server