Skip to main content
Glama
functional_error_grouping_test.go15.2 kB
package logs import ( "testing" "time" ) // Test helper to create log entries func createLogEntry(processID, processName, content string, timestamp time.Time, isError bool) LogEntry { level := LevelInfo if isError { level = LevelError } return LogEntry{ ID: processID + "-" + timestamp.Format("20060102150405.000000000"), ProcessID: processID, ProcessName: processName, Timestamp: timestamp, Content: content, IsError: isError, Level: level, } } func TestGroupErrorsByTimeLocality_EmptyInput(t *testing.T) { config := DefaultGroupingConfig() groups := GroupErrorsByTimeLocality([]LogEntry{}, config) if len(groups) != 0 { t.Errorf("Expected 0 groups for empty input, got %d", len(groups)) } } func TestGroupErrorsByTimeLocality_NoErrorEntries(t *testing.T) { baseTime := time.Now() entries := []LogEntry{ createLogEntry("process1", "test", "Info message 1", baseTime, false), createLogEntry("process1", "test", "Info message 2", baseTime.Add(100*time.Millisecond), false), } config := DefaultGroupingConfig() groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 0 { t.Errorf("Expected 0 groups for non-error entries, got %d", len(groups)) } } func TestGroupErrorsByTimeLocality_SingleErrorEntry(t *testing.T) { baseTime := time.Now() entries := []LogEntry{ createLogEntry("process1", "test", "Error: Something went wrong", baseTime, true), } config := DefaultGroupingConfig() groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 1 { t.Errorf("Expected 1 group for single error, got %d", len(groups)) return } group := groups[0] if len(group.Entries) != 1 { t.Errorf("Expected 1 entry in group, got %d", len(group.Entries)) } if group.ProcessID != "process1" { t.Errorf("Expected ProcessID 'process1', got '%s'", group.ProcessID) } if group.StartTime != baseTime { t.Errorf("Expected StartTime %v, got %v", baseTime, group.StartTime) } if group.EndTime != baseTime { t.Errorf("Expected EndTime %v, got %v", baseTime, group.EndTime) } } func TestGroupErrorsByTimeLocality_ErrorsWithinTimeThreshold(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() config.TimeGapThreshold = 200 * time.Millisecond entries := []LogEntry{ createLogEntry("process1", "test", "Error: First error", baseTime, true), createLogEntry("process1", "test", "Error: Second error", baseTime.Add(100*time.Millisecond), true), createLogEntry("process1", "test", "Error: Third error", baseTime.Add(150*time.Millisecond), true), } groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 1 { t.Errorf("Expected 1 group for errors within threshold, got %d", len(groups)) return } group := groups[0] if len(group.Entries) != 3 { t.Errorf("Expected 3 entries in group, got %d", len(group.Entries)) } if group.StartTime != baseTime { t.Errorf("Expected StartTime %v, got %v", baseTime, group.StartTime) } expectedEndTime := baseTime.Add(150 * time.Millisecond) if group.EndTime != expectedEndTime { t.Errorf("Expected EndTime %v, got %v", expectedEndTime, group.EndTime) } } func TestGroupErrorsByTimeLocality_ErrorsOutsideTimeThreshold(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() config.TimeGapThreshold = 200 * time.Millisecond entries := []LogEntry{ createLogEntry("process1", "test", "Error: First error", baseTime, true), createLogEntry("process1", "test", "Error: Second error", baseTime.Add(100*time.Millisecond), true), createLogEntry("process1", "test", "Error: Third error", baseTime.Add(500*time.Millisecond), true), // Outside threshold } groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 2 { t.Errorf("Expected 2 groups for errors outside threshold, got %d", len(groups)) return } // First group should have 2 entries if len(groups[0].Entries) != 2 { t.Errorf("Expected 2 entries in first group, got %d", len(groups[0].Entries)) } // Second group should have 1 entry if len(groups[1].Entries) != 1 { t.Errorf("Expected 1 entry in second group, got %d", len(groups[1].Entries)) } } func TestGroupErrorsByTimeLocality_MultipleProcesses(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() entries := []LogEntry{ createLogEntry("process1", "test1", "Error: Process 1 error", baseTime, true), createLogEntry("process2", "test2", "Error: Process 2 error", baseTime.Add(50*time.Millisecond), true), createLogEntry("process1", "test1", "Error: Another process 1 error", baseTime.Add(100*time.Millisecond), true), } groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 2 { t.Errorf("Expected 2 groups for different processes, got %d", len(groups)) return } // Groups should be sorted by start time // Find which group belongs to which process var process1Group, process2Group *ErrorGroup for i := range groups { if groups[i].ProcessID == "process1" { process1Group = &groups[i] } else if groups[i].ProcessID == "process2" { process2Group = &groups[i] } } if process1Group == nil { t.Error("Expected to find process1 group") return } if process2Group == nil { t.Error("Expected to find process2 group") return } if len(process1Group.Entries) != 2 { t.Errorf("Expected 2 entries in process1 group, got %d", len(process1Group.Entries)) } if len(process2Group.Entries) != 1 { t.Errorf("Expected 1 entry in process2 group, got %d", len(process2Group.Entries)) } } func TestGroupErrorsByTimeLocality_MinGroupSize(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() config.MinGroupSize = 2 // Require at least 2 entries entries := []LogEntry{ createLogEntry("process1", "test", "Error: First error", baseTime, true), createLogEntry("process1", "test", "Error: Second error", baseTime.Add(100*time.Millisecond), true), createLogEntry("process1", "test", "Error: Isolated error", baseTime.Add(1*time.Second), true), // Will be alone } groups := GroupErrorsByTimeLocality(entries, config) // Should only get the first group (2 entries), isolated error should be filtered out if len(groups) != 1 { t.Errorf("Expected 1 group with MinGroupSize=2, got %d", len(groups)) return } if len(groups[0].Entries) != 2 { t.Errorf("Expected 2 entries in group, got %d", len(groups[0].Entries)) } } func TestGroupErrorsByTimeLocality_MaxGroupSize(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() config.MaxGroupSize = 2 // Limit to 2 entries per group entries := []LogEntry{ createLogEntry("process1", "test", "Error: First error", baseTime, true), createLogEntry("process1", "test", "Error: Second error", baseTime.Add(50*time.Millisecond), true), createLogEntry("process1", "test", "Error: Third error", baseTime.Add(100*time.Millisecond), true), // Should start new group } groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 2 { t.Errorf("Expected 2 groups with MaxGroupSize=2, got %d", len(groups)) return } if len(groups[0].Entries) != 2 { t.Errorf("Expected 2 entries in first group, got %d", len(groups[0].Entries)) } if len(groups[1].Entries) != 1 { t.Errorf("Expected 1 entry in second group, got %d", len(groups[1].Entries)) } } func TestGroupErrorsByTimeLocality_MaxGroupDuration(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() config.MaxGroupDuration = 200 * time.Millisecond // Smaller duration to trigger split entries := []LogEntry{ createLogEntry("process1", "test", "Error: First error", baseTime, true), createLogEntry("process1", "test", "Error: Second error", baseTime.Add(100*time.Millisecond), true), createLogEntry("process1", "test", "Error: Third error", baseTime.Add(150*time.Millisecond), true), createLogEntry("process1", "test", "Error: Fourth error", baseTime.Add(250*time.Millisecond), true), // Within time gap but exceeds 200ms duration } groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 2 { t.Errorf("Expected 2 groups with MaxGroupDuration, got %d", len(groups)) return } // First group should have 3 entries (up to 150ms, then 250ms would exceed duration) if len(groups[0].Entries) != 3 { t.Errorf("Expected 3 entries in first group, got %d", len(groups[0].Entries)) } // Second group should have 1 entry if len(groups[1].Entries) != 1 { t.Errorf("Expected 1 entry in second group, got %d", len(groups[1].Entries)) } } func TestErrorTypeDetection(t *testing.T) { tests := []struct { content string expectedType string }{ {"TypeError: Cannot read property 'foo' of undefined", "TypeError"}, {"ReferenceError: myVar is not defined", "ReferenceError"}, {"SyntaxError: Unexpected token", "SyntaxError"}, {"Network error: ENOTFOUND", "NetworkError"}, {"MongoDB connection failed", "MongoError"}, {"ESLint: Missing semicolon", "LintError"}, {"Build failed with errors", "CompilationError"}, {"Unknown error message", "Error"}, } for _, test := range tests { result := detectErrorType(test.content) if result != test.expectedType { t.Errorf("For content '%s', expected type '%s', got '%s'", test.content, test.expectedType, result) } } } func TestSeverityDetection(t *testing.T) { tests := []struct { content string expectedSeverity string }{ {"Critical system failure", "critical"}, {"Fatal error occurred", "critical"}, {"Panic: out of memory", "critical"}, {"Error: something went wrong", "error"}, {"Build failed", "error"}, {"Warning: deprecated function", "warning"}, {"Regular error message", "error"}, // Default } for _, test := range tests { result := determineSeverity(test.content) if result != test.expectedSeverity { t.Errorf("For content '%s', expected severity '%s', got '%s'", test.content, test.expectedSeverity, result) } } } func TestMessageExtraction(t *testing.T) { tests := []struct { input string expected string }{ {"[12:34:56] Error: Something went wrong", "Error: Something went wrong"}, {"(process) TypeError: Cannot read property", "TypeError: Cannot read property"}, {"Simple error message", "Simple error message"}, {"", ""}, } for _, test := range tests { result := extractMainMessage(test.input) if result != test.expected { t.Errorf("For input '%s', expected '%s', got '%s'", test.input, test.expected, result) } } } func TestFilterErrorEntries(t *testing.T) { baseTime := time.Now() entries := []LogEntry{ createLogEntry("process1", "test", "Info message", baseTime, false), createLogEntry("process1", "test", "Error message", baseTime.Add(100*time.Millisecond), true), { ID: "test3", ProcessID: "process1", Timestamp: baseTime.Add(200 * time.Millisecond), Content: "Warning message", IsError: false, Level: LevelWarn, // This should be included as it's >= LevelError }, { ID: "test4", ProcessID: "process1", Timestamp: baseTime.Add(300 * time.Millisecond), Content: "Error level message", IsError: false, Level: LevelError, // This should be included }, } errorEntries := filterErrorEntries(entries) // Should have 2 entries: the explicit error and the LevelError entry if len(errorEntries) != 2 { t.Errorf("Expected 2 error entries, got %d", len(errorEntries)) } } func TestGroupingSortedByTime(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() config.TimeGapThreshold = 200 * time.Millisecond // Smaller gap to create separate groups // Create entries in non-chronological order with gaps > 200ms between them entries := []LogEntry{ createLogEntry("process1", "test", "Error: Third", baseTime.Add(600*time.Millisecond), true), createLogEntry("process1", "test", "Error: First", baseTime, true), createLogEntry("process1", "test", "Error: Second", baseTime.Add(300*time.Millisecond), true), } groups := GroupErrorsByTimeLocality(entries, config) if len(groups) != 3 { t.Errorf("Expected 3 groups, got %d", len(groups)) return } // Groups should be sorted by start time if !groups[0].StartTime.Before(groups[1].StartTime) { t.Error("Groups not sorted by start time") } if !groups[1].StartTime.Before(groups[2].StartTime) { t.Error("Groups not sorted by start time") } // Check content order - groups should be sorted by start time if groups[0].Entries[0].Content != "Error: First" { t.Errorf("Expected first group to contain 'Error: First', got '%s'", groups[0].Entries[0].Content) } if groups[1].Entries[0].Content != "Error: Second" { t.Errorf("Expected second group to contain 'Error: Second', got '%s'", groups[1].Entries[0].Content) } if groups[2].Entries[0].Content != "Error: Third" { t.Errorf("Expected third group to contain 'Error: Third', got '%s'", groups[2].Entries[0].Content) } } func TestComplexScenario(t *testing.T) { baseTime := time.Now() config := DefaultGroupingConfig() config.TimeGapThreshold = 200 * time.Millisecond config.MinGroupSize = 1 config.MaxGroupSize = 3 // Complex scenario: Multiple processes, mixed timing, some grouped, some isolated entries := []LogEntry{ // Process 1: Group of 2 createLogEntry("process1", "frontend", "TypeError: Cannot read property", baseTime, true), createLogEntry("process1", "frontend", " at Object.render", baseTime.Add(50*time.Millisecond), true), // Process 2: Isolated error createLogEntry("process2", "backend", "MongoDB connection error", baseTime.Add(100*time.Millisecond), true), // Process 1: Another group (after gap) createLogEntry("process1", "frontend", "SyntaxError: Unexpected token", baseTime.Add(500*time.Millisecond), true), createLogEntry("process1", "frontend", " at compile", baseTime.Add(550*time.Millisecond), true), createLogEntry("process1", "frontend", " at build", baseTime.Add(600*time.Millisecond), true), // Info message (should be filtered out) createLogEntry("process1", "frontend", "Info: Build completed", baseTime.Add(650*time.Millisecond), false), // Process 1: Isolated error after gap createLogEntry("process1", "frontend", "Critical: Memory exhausted", baseTime.Add(1*time.Second), true), } groups := GroupErrorsByTimeLocality(entries, config) // Expected: 4 groups total // - Process1: group of 2 (TypeError + stack) // - Process2: group of 1 (MongoDB error) // - Process1: group of 3 (SyntaxError + 2 stack lines) // - Process1: group of 1 (Critical error) if len(groups) != 4 { t.Errorf("Expected 4 groups in complex scenario, got %d", len(groups)) return } // Verify group contents expectedSizes := []int{2, 1, 3, 1} for i, expectedSize := range expectedSizes { if len(groups[i].Entries) != expectedSize { t.Errorf("Group %d: expected %d entries, got %d", i, expectedSize, len(groups[i].Entries)) } } // Verify error types are detected expectedTypes := []string{"TypeError", "MongoError", "SyntaxError", "Error"} for i, expectedType := range expectedTypes { if groups[i].ErrorType != expectedType { t.Errorf("Group %d: expected error type '%s', got '%s'", i, expectedType, groups[i].ErrorType) } } }

Latest Blog Posts

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/standardbeagle/brummer'

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