Skip to main content
Glama
ai_coder_pty_view_test.go11.5 kB
package tui import ( "fmt" "testing" "time" "github.com/standardbeagle/brummer/internal/aicoder" "github.com/standardbeagle/brummer/internal/logs" "github.com/standardbeagle/brummer/internal/proxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAICoderPTYView_GetTerminalSize(t *testing.T) { tests := []struct { name string width int height int isFullScreen bool hasStatusMessage bool showHelp bool wantWidth int wantHeight int description string }{ { name: "uninitialized dimensions return defaults", width: 0, height: 0, isFullScreen: false, wantWidth: 80, wantHeight: 24, description: "Zero dimensions should trigger fallback to standard terminal size", }, { name: "negative width calculation triggers width fallback", width: 5, // 5 - 4 - 2 = -1 (negative) height: 10, isFullScreen: false, wantWidth: 80, // Falls back to default wantHeight: 5, // 10 - 3 - 2 = 5 (valid) description: "Width calculation resulting in negative should use default width", }, { name: "negative height calculation triggers height fallback", width: 50, height: 4, // 4 - 3 - 2 = -1 (negative) isFullScreen: false, wantWidth: 44, // 50 - 4 - 2 = 44 (valid) wantHeight: 24, // Falls back to default description: "Height calculation resulting in negative should use default height", }, { name: "valid windowed dimensions with base layout", width: 100, height: 40, isFullScreen: false, wantWidth: 94, // 100 - 4 - 2 = 94 wantHeight: 35, // 40 - 3 - 2 = 35 description: "Normal windowed mode with header (3) + footer (2) deductions", }, { name: "windowed with status message increases header", width: 100, height: 40, isFullScreen: false, hasStatusMessage: true, wantWidth: 94, // 100 - 4 - 2 = 94 wantHeight: 33, // 40 - 5 - 2 = 33 (header+2 for status) description: "Status message adds 2 lines to header calculation", }, { name: "windowed with help enabled increases footer", width: 100, height: 40, isFullScreen: false, showHelp: true, wantWidth: 94, // 100 - 4 - 2 = 94 wantHeight: 27, // 40 - 3 - 10 = 27 (footer = 10 for help) description: "Help mode uses extended footer (10 lines)", }, { name: "fullscreen uses maximum space", width: 100, height: 40, isFullScreen: true, wantWidth: 96, // 100 - 4 = 96 wantHeight: 36, // 40 - 4 = 36 description: "Fullscreen minimizes UI overhead for maximum terminal space", }, { name: "fullscreen with minimal dimensions triggers fallbacks", width: 3, height: 3, isFullScreen: true, wantWidth: 80, // 3 - 4 = -1, fallback to 80 wantHeight: 24, // 3 - 4 = -1, fallback to 24 description: "Small fullscreen dimensions should use safe defaults", }, { name: "edge case: exactly minimum windowed width", width: 6, // 6 - 4 - 2 = 0, triggers fallback height: 20, isFullScreen: false, wantWidth: 80, // Fallback for width <= 0 wantHeight: 15, // 20 - 3 - 2 = 15 description: "Width calculation of exactly 0 should trigger fallback", }, { name: "edge case: exactly minimum windowed height", width: 50, height: 5, // 5 - 3 - 2 = 0, triggers fallback isFullScreen: false, wantWidth: 44, // 50 - 4 - 2 = 44 wantHeight: 24, // Fallback for height <= 0 description: "Height calculation of exactly 0 should trigger fallback", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a mock PTY manager mockDataProvider := &mockBrummerDataProvider{} mockEventBus := &mockEventBus{} ptyManager := aicoder.NewPTYManager(mockDataProvider, mockEventBus) view := NewAICoderPTYView(ptyManager) view.width = tt.width view.height = tt.height view.isFullScreen = tt.isFullScreen // Set up status message if needed if tt.hasStatusMessage { view.statusMessage = "Test status message" view.statusTime = time.Now() // Current time ensures it's within 3s window } // Set help mode if needed view.showHelp = tt.showHelp gotWidth, gotHeight := view.getTerminalSize() assert.Equal(t, tt.wantWidth, gotWidth, "width mismatch for case: %s", tt.description) assert.Equal(t, tt.wantHeight, gotHeight, "height mismatch for case: %s", tt.description) assert.Greater(t, gotWidth, 0, "width should be positive") assert.Greater(t, gotHeight, 0, "height should be positive") }) } } func TestAICoderPTYView_SessionLifecycle(t *testing.T) { t.Run("attach to session before window size initialization", func(t *testing.T) { // This test simulates the exact scenario that caused the original panic mockDataProvider := &mockBrummerDataProvider{} mockEventBus := &mockEventBus{} ptyManager := aicoder.NewPTYManager(mockDataProvider, mockEventBus) view := NewAICoderPTYView(ptyManager) // Simulate uninitialized view (width/height = 0) view.width = 0 view.height = 0 // Create a PTY session session, err := ptyManager.CreateSession("test", "echo", []string{"hello"}) require.NoError(t, err) require.NotNil(t, session) defer session.Close() // This should not panic even with uninitialized dimensions require.NotPanics(t, func() { err = view.AttachToSession(session.ID) }) assert.NoError(t, err) assert.Equal(t, session, view.currentSession, "session should be attached") }) t.Run("session resize with negative dimensions uses defaults", func(t *testing.T) { mockDataProvider := &mockBrummerDataProvider{} mockEventBus := &mockEventBus{} ptyManager := aicoder.NewPTYManager(mockDataProvider, mockEventBus) view := NewAICoderPTYView(ptyManager) // Create a PTY session session, err := ptyManager.CreateSession("test", "sh", []string{"-c", "echo test"}) require.NoError(t, err) defer session.Close() view.SetCurrentSession(session) // Should not panic with negative dimensions require.NotPanics(t, func() { view.width = -10 view.height = -5 width, height := view.getTerminalSize() // Should return default dimensions assert.Equal(t, 80, width) assert.Equal(t, 24, height) }) }) t.Run("detach from session clears current session", func(t *testing.T) { mockDataProvider := &mockBrummerDataProvider{} mockEventBus := &mockEventBus{} ptyManager := aicoder.NewPTYManager(mockDataProvider, mockEventBus) view := NewAICoderPTYView(ptyManager) view.width = 80 view.height = 24 // Create and attach to session session, err := ptyManager.CreateSession("test", "echo", []string{"hello"}) require.NoError(t, err) defer session.Close() err = view.AttachToSession(session.ID) require.NoError(t, err) assert.NotNil(t, view.currentSession) // Detach from session (simulate session closing) view.currentSession = nil view.UnfocusTerminal() assert.Nil(t, view.currentSession, "current session should be cleared") assert.False(t, view.terminalFocused, "terminal should not be focused after detach") }) } func TestAICoderPTYView_ScrollFunctionality(t *testing.T) { t.Run("mouse wheel scrolling with bounds checking", func(t *testing.T) { mockDataProvider := &mockBrummerDataProvider{} mockEventBus := &mockEventBus{} ptyManager := aicoder.NewPTYManager(mockDataProvider, mockEventBus) view := NewAICoderPTYView(ptyManager) view.width = 80 view.height = 24 // Create session with some output history session, err := ptyManager.CreateSession("test", "echo", []string{"hello"}) require.NoError(t, err) defer session.Close() view.SetCurrentSession(session) // Mock some history data - simulate multiple lines of output historyData := make([]byte, 0) for i := 0; i < 50; i++ { historyData = append(historyData, []byte(fmt.Sprintf("Line %d\n", i))...) } // Initial scroll offset should be 0 assert.Equal(t, 0, view.scrollOffset, "initial scroll offset should be 0") // Test mouse wheel up (scroll up) initialOffset := view.scrollOffset view.scrollOffset += 3 // Simulate mouse wheel up assert.Greater(t, view.scrollOffset, initialOffset, "scroll up should increase offset") // Test mouse wheel down (scroll down) view.scrollOffset -= 3 // Simulate mouse wheel down assert.Equal(t, initialOffset, view.scrollOffset, "scroll down should decrease offset") // Test scroll down beyond 0 (should clamp to 0) view.scrollOffset = 1 view.scrollOffset -= 3 if view.scrollOffset < 0 { view.scrollOffset = 0 } assert.Equal(t, 0, view.scrollOffset, "scroll offset should not go below 0") }) t.Run("page up/down scrolling respects bounds", func(t *testing.T) { mockDataProvider := &mockBrummerDataProvider{} mockEventBus := &mockEventBus{} ptyManager := aicoder.NewPTYManager(mockDataProvider, mockEventBus) view := NewAICoderPTYView(ptyManager) view.width = 80 view.height = 24 session, err := ptyManager.CreateSession("test", "echo", []string{"hello"}) require.NoError(t, err) defer session.Close() view.SetCurrentSession(session) _, termHeight := view.getTerminalSize() halfPage := termHeight / 2 // Test page up initialOffset := view.scrollOffset view.scrollOffset += halfPage assert.Equal(t, halfPage, view.scrollOffset, "page up should scroll half terminal height") // Test page down view.scrollOffset -= halfPage assert.Equal(t, initialOffset, view.scrollOffset, "page down should return to initial position") // Test page down beyond 0 (should clamp to 0) view.scrollOffset = 5 view.scrollOffset -= halfPage if view.scrollOffset < 0 { view.scrollOffset = 0 } assert.Equal(t, 0, view.scrollOffset, "page down should not scroll below 0") }) t.Run("auto-scroll on new content", func(t *testing.T) { mockDataProvider := &mockBrummerDataProvider{} mockEventBus := &mockEventBus{} ptyManager := aicoder.NewPTYManager(mockDataProvider, mockEventBus) view := NewAICoderPTYView(ptyManager) view.width = 80 view.height = 24 // Set some scroll offset view.scrollOffset = 10 // Simulate new content arriving (this typically resets scroll) view.addToScrollback("New content line") // Auto-scroll should reset offset to 0 when new content arrives assert.Equal(t, 0, view.scrollOffset, "new content should trigger auto-scroll to bottom") }) } // Mock implementations for testing type mockBrummerDataProvider struct{} func (m *mockBrummerDataProvider) GetLastError() *logs.ErrorContext { return nil } func (m *mockBrummerDataProvider) GetRecentLogs(count int) []logs.LogEntry { return nil } func (m *mockBrummerDataProvider) GetTestFailures() interface{} { return nil } func (m *mockBrummerDataProvider) GetBuildOutput() string { return "" } func (m *mockBrummerDataProvider) GetProcessInfo() interface{} { return nil } func (m *mockBrummerDataProvider) GetDetectedURLs() []logs.URLEntry { return nil } func (m *mockBrummerDataProvider) GetRecentProxyRequests(count int) []*proxy.Request { return nil } type mockEventBus struct{} func (m *mockEventBus) Emit(event string, data interface{}) {}

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