Files
gastown/internal/web/fetcher_test.go
Mike Lady f30f46192f test(dashboard): Add unit tests for convoy dashboard fetcher
Add table-driven tests for:
- calculateWorkStatus: complete, active, stale, stuck, waiting states
- determineCIStatus: pass, fail, pending with various check combinations
- determineMergeableStatus: ready, conflict, pending states
- determineColorClass: mq-green, mq-yellow, mq-red combinations
- getRefineryStatusHint: idle, singular, multiple PR messages
- truncateStatusHint: line truncation to 60 chars with ellipsis
- parsePolecatSessionName: gt-<rig>-<polecat> parsing
- isWorkerSession: worker vs non-worker session detection
- parseActivityTimestamp: Unix timestamp parsing from tmux

Also refactors inline logic into testable helper functions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 18:23:45 -08:00

418 lines
10 KiB
Go

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)
}
})
}
}