package web import ( "testing" "github.com/steveyegge/gastown/internal/activity" ) func TestCalculateWorkStatus(t *testing.T) { tests := []struct { name string completed int total int activityColor string want string }{ { name: "complete when all done", completed: 5, total: 5, activityColor: activity.ColorGreen, want: "complete", }, { name: "complete overrides activity color", completed: 3, total: 3, activityColor: activity.ColorRed, want: "complete", }, { name: "active when green", completed: 2, total: 5, activityColor: activity.ColorGreen, want: "active", }, { name: "stale when yellow", completed: 2, total: 5, activityColor: activity.ColorYellow, want: "stale", }, { name: "stuck when red", completed: 2, total: 5, activityColor: activity.ColorRed, want: "stuck", }, { name: "waiting when unknown color", completed: 2, total: 5, activityColor: activity.ColorUnknown, want: "waiting", }, { name: "waiting when empty color", completed: 0, total: 5, activityColor: "", want: "waiting", }, { name: "waiting when no work yet", completed: 0, total: 0, activityColor: activity.ColorUnknown, want: "waiting", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := calculateWorkStatus(tt.completed, tt.total, tt.activityColor) if got != tt.want { t.Errorf("calculateWorkStatus(%d, %d, %q) = %q, want %q", tt.completed, tt.total, tt.activityColor, got, tt.want) } }) } } func TestDetermineCIStatus(t *testing.T) { tests := []struct { name string checks []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` } want string }{ { name: "pending when no checks", checks: nil, want: "pending", }, { name: "pass when all success", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Conclusion: "success"}, {Conclusion: "success"}, }, want: "pass", }, { name: "pass with skipped checks", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Conclusion: "success"}, {Conclusion: "skipped"}, }, want: "pass", }, { name: "fail when any failure", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Conclusion: "success"}, {Conclusion: "failure"}, }, want: "fail", }, { name: "fail when cancelled", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Conclusion: "cancelled"}, }, want: "fail", }, { name: "fail when timed_out", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Conclusion: "timed_out"}, }, want: "fail", }, { name: "pending when in_progress", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Conclusion: "success"}, {Status: "in_progress"}, }, want: "pending", }, { name: "pending when queued", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Status: "queued"}, }, want: "pending", }, { name: "fail from state FAILURE", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {State: "FAILURE"}, }, want: "fail", }, { name: "pending from state PENDING", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {State: "PENDING"}, }, want: "pending", }, { name: "failure takes precedence over pending", checks: []struct { State string `json:"state"` Status string `json:"status"` Conclusion string `json:"conclusion"` }{ {Conclusion: "failure"}, {Status: "in_progress"}, }, want: "fail", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := determineCIStatus(tt.checks) if got != tt.want { t.Errorf("determineCIStatus() = %q, want %q", got, tt.want) } }) } } func TestDetermineMergeableStatus(t *testing.T) { tests := []struct { name string mergeable string want string }{ {"ready when MERGEABLE", "MERGEABLE", "ready"}, {"ready when lowercase mergeable", "mergeable", "ready"}, {"conflict when CONFLICTING", "CONFLICTING", "conflict"}, {"conflict when lowercase conflicting", "conflicting", "conflict"}, {"pending when UNKNOWN", "UNKNOWN", "pending"}, {"pending when empty", "", "pending"}, {"pending when other value", "something_else", "pending"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := determineMergeableStatus(tt.mergeable) if got != tt.want { t.Errorf("determineMergeableStatus(%q) = %q, want %q", tt.mergeable, got, tt.want) } }) } } func TestDetermineColorClass(t *testing.T) { tests := []struct { name string ciStatus string mergeable string want string }{ {"green when pass and ready", "pass", "ready", "mq-green"}, {"red when CI fails", "fail", "ready", "mq-red"}, {"red when conflict", "pass", "conflict", "mq-red"}, {"red when both fail and conflict", "fail", "conflict", "mq-red"}, {"yellow when CI pending", "pending", "ready", "mq-yellow"}, {"yellow when merge pending", "pass", "pending", "mq-yellow"}, {"yellow when both pending", "pending", "pending", "mq-yellow"}, {"yellow for unknown states", "unknown", "unknown", "mq-yellow"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := determineColorClass(tt.ciStatus, tt.mergeable) if got != tt.want { t.Errorf("determineColorClass(%q, %q) = %q, want %q", tt.ciStatus, tt.mergeable, got, tt.want) } }) } } func TestGetRefineryStatusHint(t *testing.T) { // Create a minimal fetcher for testing f := &LiveConvoyFetcher{} tests := []struct { name string mergeQueueCount int want string }{ {"idle when no PRs", 0, "Idle - Waiting for PRs"}, {"singular PR", 1, "Processing 1 PR"}, {"multiple PRs", 2, "Processing 2 PRs"}, {"many PRs", 10, "Processing 10 PRs"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := f.getRefineryStatusHint(tt.mergeQueueCount) if got != tt.want { t.Errorf("getRefineryStatusHint(%d) = %q, want %q", tt.mergeQueueCount, got, tt.want) } }) } } func TestTruncateStatusHint(t *testing.T) { tests := []struct { name string input string want string }{ {"short line unchanged", "Hello world", "Hello world"}, {"exactly 60 chars unchanged", "123456789012345678901234567890123456789012345678901234567890", "123456789012345678901234567890123456789012345678901234567890"}, {"61 chars truncated", "1234567890123456789012345678901234567890123456789012345678901", "123456789012345678901234567890123456789012345678901234567..."}, {"long line truncated", "This is a very long line that should be truncated because it exceeds sixty characters", "This is a very long line that should be truncated because..."}, {"empty string", "", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := truncateStatusHint(tt.input) if got != tt.want { t.Errorf("truncateStatusHint(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestParsePolecatSessionName(t *testing.T) { tests := []struct { name string sessionName string wantRig string wantPolecat string wantOk bool }{ {"valid polecat session", "gt-roxas-dag", "roxas", "dag", true}, {"valid polecat with hyphen", "gt-gas-town-nux", "gas", "town-nux", true}, {"refinery session", "gt-roxas-refinery", "roxas", "refinery", true}, {"witness session", "gt-gastown-witness", "gastown", "witness", true}, {"not gt prefix", "other-roxas-dag", "", "", false}, {"too few parts", "gt-roxas", "", "", false}, {"empty string", "", "", "", false}, {"single gt", "gt", "", "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rig, polecat, ok := parsePolecatSessionName(tt.sessionName) if ok != tt.wantOk { t.Errorf("parsePolecatSessionName(%q) ok = %v, want %v", tt.sessionName, ok, tt.wantOk) } if ok && (rig != tt.wantRig || polecat != tt.wantPolecat) { t.Errorf("parsePolecatSessionName(%q) = (%q, %q), want (%q, %q)", tt.sessionName, rig, polecat, tt.wantRig, tt.wantPolecat) } }) } } func TestIsWorkerSession(t *testing.T) { tests := []struct { name string polecat string wantWork bool }{ {"polecat dag is worker", "dag", true}, {"polecat nux is worker", "nux", true}, {"refinery is worker", "refinery", true}, {"witness is not worker", "witness", false}, {"mayor is not worker", "mayor", false}, {"deacon is not worker", "deacon", false}, {"boot is not worker", "boot", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isWorkerSession(tt.polecat) if got != tt.wantWork { t.Errorf("isWorkerSession(%q) = %v, want %v", tt.polecat, got, tt.wantWork) } }) } } func TestParseActivityTimestamp(t *testing.T) { tests := []struct { name string input string wantUnix int64 wantValid bool }{ {"valid timestamp", "1704312345", 1704312345, true}, {"zero timestamp", "0", 0, false}, {"empty string", "", 0, false}, {"invalid string", "abc", 0, false}, {"negative", "-123", 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { unix, valid := parseActivityTimestamp(tt.input) if valid != tt.wantValid { t.Errorf("parseActivityTimestamp(%q) valid = %v, want %v", tt.input, valid, tt.wantValid) } if valid && unix != tt.wantUnix { t.Errorf("parseActivityTimestamp(%q) = %d, want %d", tt.input, unix, tt.wantUnix) } }) } }