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>
This commit is contained in:
@@ -710,3 +710,46 @@ func (f *LiveConvoyFetcher) getRefineryStatusHint(mergeQueueCount int) string {
|
||||
}
|
||||
return fmt.Sprintf("Processing %d PRs", mergeQueueCount)
|
||||
}
|
||||
|
||||
// truncateStatusHint truncates a status hint to 60 characters with ellipsis.
|
||||
func truncateStatusHint(line string) string {
|
||||
if len(line) > 60 {
|
||||
return line[:57] + "..."
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// parsePolecatSessionName parses a tmux session name into rig and polecat components.
|
||||
// Format: gt-<rig>-<polecat> -> (rig, polecat, true)
|
||||
// Returns ("", "", false) if the format is invalid.
|
||||
func parsePolecatSessionName(sessionName string) (rig, polecat string, ok bool) {
|
||||
if !strings.HasPrefix(sessionName, "gt-") {
|
||||
return "", "", false
|
||||
}
|
||||
parts := strings.SplitN(sessionName, "-", 3)
|
||||
if len(parts) != 3 {
|
||||
return "", "", false
|
||||
}
|
||||
return parts[1], parts[2], true
|
||||
}
|
||||
|
||||
// isWorkerSession returns true if the polecat name represents a worker session.
|
||||
// Non-worker sessions: witness, mayor, deacon, boot
|
||||
func isWorkerSession(polecat string) bool {
|
||||
switch polecat {
|
||||
case "witness", "mayor", "deacon", "boot":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// parseActivityTimestamp parses a Unix timestamp string from tmux.
|
||||
// Returns (0, false) for invalid or zero timestamps.
|
||||
func parseActivityTimestamp(s string) (int64, bool) {
|
||||
var unix int64
|
||||
if _, err := fmt.Sscanf(s, "%d", &unix); err != nil || unix <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
return unix, true
|
||||
}
|
||||
|
||||
417
internal/web/fetcher_test.go
Normal file
417
internal/web/fetcher_test.go
Normal file
@@ -0,0 +1,417 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user