212 lines
6.9 KiB
Go
212 lines
6.9 KiB
Go
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()
|
|
second, 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())
|
|
}
|
|
claimedID := coordinator.supersede()
|
|
select {
|
|
case <-second.Done():
|
|
default:
|
|
t.Fatal("claimed snapshot did not cancel the active request")
|
|
}
|
|
if coordinator.current(secondID) || !coordinator.current(claimedID) {
|
|
t.Fatalf("claimed request ids: second=%t claimed=%t",
|
|
coordinator.current(secondID), coordinator.current(claimedID))
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|