package main import ( "context" "errors" "strings" "testing" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/ansi" ) type healthTestService struct { components []HealthComponent rate RateLimitSnapshot } func (s *healthTestService) ListPullRequests( context.Context, string, string, int, bool, ) ([]PullRequest, error) { return nil, nil } func (s *healthTestService) GetPullRequest( context.Context, string, string, int, ) (PRDetails, error) { return PRDetails{}, nil } func (s *healthTestService) HealthReport() []HealthComponent { return append([]HealthComponent(nil), s.components...) } func (s *healthTestService) RateLimit() RateLimitSnapshot { return s.rate } func TestHealthScreenReportsComponentsRateLimitAndEvents(t *testing.T) { service := &healthTestService{ components: []HealthComponent{{ Name: "GitHub API", Level: healthOK, Summary: "request succeeded", }}, rate: RateLimitSnapshot{ Limit: 5000, Remaining: 42, UpdatedAt: time.Now(), ResetAt: time.Now().Add(time.Hour), }, } settings := defaultAppSettings() settings.ReadState = loadReadState(t.TempDir() + "/state.json") settings.Drafts = loadDraftStore(t.TempDir() + "/drafts.json") app := NewAppWithSettings(service, "o", "r", false, 50, time.Minute, settings) app.width, app.height = 64, 30 app.details = PRDetails{ PullRequest: PullRequest{ID: "pr", UpdatedAt: time.Now()}, DataIssues: []DataIssue{{Component: "timeline", Message: "unavailable"}}, } app.recordHealth("timeline", healthWarning, "a deliberately long warning that must remain readable") lines := app.healthLines() plain := ansi.Strip(strings.Join(lines, "\n")) for _, wanted := range []string{ "configuration", "read state", "draft persistence", "GitHub API", "rate limit", "42/5000", "PR core data", "timeline", } { if !strings.Contains(plain, wanted) { t.Fatalf("health output missing %q:\n%s", wanted, plain) } } for _, line := range lines { if ansi.StringWidth(line) > app.width-2 { t.Fatalf("health line width = %d, want <= %d: %q", ansi.StringWidth(line), app.width-2, line) } } } func TestHealthScreenOpensAndReturnsToPreviousScreen(t *testing.T) { app := NewApp(nil, "", "", false, 50, time.Minute) app.screen = dashboardScreen app.scroll = 7 app.width, app.height = 100, 30 updated, _ := app.Update(runeKey("H")) app = updated.(App) if app.screen != healthScreen || app.healthReturn != dashboardScreen || app.scroll != 7 { t.Fatalf("health navigation = screen %v return %v", app.screen, app.healthReturn) } view := ansi.Strip(app.View()) if !strings.Contains(view, "Application health") || !strings.Contains(view, "╭") || !strings.Contains(view, "╰") { t.Fatalf("health is not rendered as a modal:\n%s", view) } updated, _ = app.Update(tea.KeyMsg{Type: tea.KeyEsc}) app = updated.(App) if app.screen != dashboardScreen || app.scroll != 7 { t.Fatalf("health back returned to screen %v at scroll %d", app.screen, app.scroll) } } func TestHealthRefreshLineIsStableAndInformational(t *testing.T) { app := NewApp(nil, "", "", false, 50, time.Minute) app.width, app.height = 100, 30 app.loading = false idle := app.healthLines() app.loading = true loading := app.healthLines() findRefresh := func(lines []string) (int, string) { for index, line := range lines { plain := ansi.Strip(line) if strings.Contains(plain, "refresh") { return index, plain } } return -1, "" } idleIndex, idleLine := findRefresh(idle) loadingIndex, loadingLine := findRefresh(loading) if idleIndex < 0 || idleIndex != loadingIndex { t.Fatalf("refresh line moved from %d to %d", idleIndex, loadingIndex) } if !strings.Contains(idleLine, "OK") || !strings.Contains(loadingLine, "INFO") || strings.Contains(loadingLine, "WARN") { t.Fatalf("refresh states: idle=%q loading=%q", idleLine, loadingLine) } } func TestAdaptivePollingHonorsRateLimitBackoff(t *testing.T) { now := time.Unix(1000, 0) service := &healthTestService{rate: RateLimitSnapshot{ Limit: 5000, Remaining: 100, UpdatedAt: now, }} app := NewApp(service, "", "", false, 50, 10*time.Second) got := app.adaptivePollInterval(now) if got < 72*time.Second || got > 88*time.Second { t.Fatalf("low-budget interval = %s, want about 80s with jitter", got) } service.rate.RetryAfter = now.Add(2 * time.Minute) got = app.adaptivePollInterval(now) if got < 108*time.Second || got > 132*time.Second { t.Fatalf("retry-after interval = %s, want about 2m with jitter", got) } } func TestRequestCoordinatorCancelsSupersededRequest(t *testing.T) { var coordinator requestCoordinator first, cancelFirst, firstID := coordinator.start(time.Minute) defer cancelFirst() _, cancelSecond, secondID := coordinator.start(time.Minute) defer cancelSecond() select { case <-first.Done(): default: t.Fatal("superseded request context was not canceled") } if coordinator.current(firstID) || !coordinator.current(secondID) { t.Fatalf("current request ids: first=%t second=%t", coordinator.current(firstID), coordinator.current(secondID)) } if !errors.Is(first.Err(), context.Canceled) { t.Fatalf("first context error = %v", first.Err()) } } func TestPartialRefreshPreservesLastCompleteSubsections(t *testing.T) { previous := PRDetails{ PullRequest: PullRequest{Owner: "o", Repository: "r", Number: 1}, HeadOID: "head", BaseOID: "base", Threads: []ReviewThread{{ID: "old-thread"}, {ID: "second-thread"}}, Conversation: []PRComment{{ID: "old-comment"}, {ID: "second-comment"}}, ConflictFiles: []string{"conflicted.go"}, Checks: []Check{{ ID: "check", Annotations: []CheckAnnotation{{Path: "problem.go"}}, }}, } fresh := PRDetails{ PullRequest: PullRequest{Owner: "o", Repository: "r", Number: 1}, HeadOID: "head", BaseOID: "base", Threads: []ReviewThread{{ID: "first-page-only"}}, Conversation: []PRComment{{ID: "first-page-only"}}, Checks: []Check{{ID: "check"}}, DataIssues: []DataIssue{ {Component: "review threads", Message: "page failed"}, {Component: "conversation", Message: "page failed"}, }, } merged := preservePartialPRData(fresh, previous) if len(merged.Threads) != 2 || merged.Threads[0].ID != "old-thread" { t.Fatalf("preserved threads = %#v", merged.Threads) } if len(merged.Conversation) != 2 || merged.Conversation[0].ID != "old-comment" { t.Fatalf("preserved conversation = %#v", merged.Conversation) } if len(merged.DataIssues) != 2 { t.Fatalf("partial markers were lost: %#v", merged.DataIssues) } if len(merged.ConflictFiles) != 1 || len(merged.Checks[0].Annotations) != 1 { t.Fatalf("secondary data flickered during core refresh: %#v", merged) } }