package web import ( "fmt" "testing" "time" "bugabinga.net/luci/internal/history" "bugabinga.net/luci/internal/inbox" "bugabinga.net/luci/internal/proquint" "bugabinga.net/luci/internal/runstate" "bugabinga.net/luci/internal/trigger" ) func TestBuildDashboardProjectsLatestRepositoryHealth(t *testing.T) { now := time.Date(2026, 8, 5, 12, 0, 0, 0, time.UTC) events := []history.Event{ {RunID: "1", ChildID: "run", Repo: "fixed", Job: "push", Status: "failed", Time: now.Add(-3 * time.Minute)}, {RunID: "2", ChildID: "run", Repo: "fixed", Job: "push", Status: "success", Rev: "1234567890", Time: now.Add(-time.Minute), StartedAt: now.Add(-time.Minute - time.Second)}, {RunID: "3", ChildID: "run", Repo: "broken", Job: "push", Status: "failed", Time: now.Add(-2 * time.Minute)}, {RunID: "3", ChildID: "001", Repo: "broken", Job: "smoke", Status: "failed", Time: now.Add(-2 * time.Minute)}, } queuedID := fmt.Sprintf("%d", now.Add(-10*time.Second).UnixNano()) pending := []inbox.Pending{{ID: queuedID, Phase: "queued", Event: trigger.PushEvent{Kind: "push", Repo: "queued", New: "abcdef123", Ref: "refs/heads/trunk"}}} view := buildDashboard(now, events, pending, nil, false) if view.FailingRepos != 1 || view.RunningRepos != 1 || view.HealthyRepos != 1 { t.Fatalf("counts=%#v", view) } if len(view.Attention) != 1 || view.Attention[0].Repo != "broken" { t.Fatalf("attention=%#v", view.Attention) } if len(view.Progress) != 1 || view.Progress[0].Repo != "queued" || view.Progress[0].Timing != "10s" { t.Fatalf("progress=%#v", view.Progress) } if len(view.Success) != 1 || view.Success[0].Rev != "1234" || view.Success[0].Timing != "1s" { t.Fatalf("success=%#v", view.Success) } } func TestBuildDashboardCountsFailingActiveRepoOnce(t *testing.T) { now := time.Now().UTC() events := []history.Event{{RunID: "1", ChildID: "run", Repo: "repo", Job: "push", Status: "failed", Time: now.Add(-time.Minute)}} active := []runstate.Active{{RunID: "2", ChildID: "001", Repo: "repo", Job: "smoke", StartedAt: now.Add(-5 * time.Second)}} view := buildDashboard(now, events, nil, active, false) if view.FailingRepos != 1 || view.RunningRepos != 0 || len(view.Progress) != 1 { t.Fatalf("view=%#v", view) } } func TestBuildDashboardLimitsAndAll(t *testing.T) { now := time.Now().UTC() var events []history.Event for i := range 55 { events = append(events, history.Event{RunID: fmt.Sprint(i), ChildID: "run", Repo: fmt.Sprintf("repo-%02d", i), Job: "push", Status: "success", Time: now.Add(-time.Duration(i) * time.Second)}) } if got := len(buildDashboard(now, events, nil, nil, false).Success); got != 50 { t.Fatalf("limited=%d", got) } if got := len(buildDashboard(now, events, nil, nil, true).Success); got != 55 { t.Fatalf("all=%d", got) } } func TestBuildRunResolvesPendingActiveAndTerminal(t *testing.T) { now := time.Now().UTC() id := fmt.Sprint(now.UnixNano()) pending := []inbox.Pending{{ID: id, Phase: "preparing", Event: trigger.ManualEvent{Kind: "manual", Repo: "repo", Job: "check", Rev: ptr("abc")}}} got := buildRun(id, now, nil, pending, nil) if !got.Known || got.Parent.Status != "preparing" || got.Parent.Repo != "repo" { t.Fatalf("pending=%#v", got) } active := []runstate.Active{{RunID: id, ChildID: "001", Repo: "repo", Job: "check", Rev: "abcdef123", StartedAt: now.Add(-time.Second)}} got = buildRun(id, now, nil, pending, active) if got.Parent.Status != "running" || len(got.Children) != 1 { t.Fatalf("active=%#v", got) } events := []history.Event{{RunID: id, ChildID: "run", Repo: "repo", Job: "check", Status: "success", Time: now}, {RunID: id, ChildID: "001", Repo: "repo", Job: "check", Status: "success", Time: now}} got = buildRun(id, now, events, nil, nil) if got.Parent.Status != "success" || len(got.Children) != 1 { t.Fatalf("terminal=%#v", got) } } func TestFormattersUseCompactValuesAndPlaceholders(t *testing.T) { if shortRev("") != "—" || shortRev("abc") != "abc" || shortRev("123456789") != "1234567" { t.Fatal("short rev") } for duration, want := range map[time.Duration]string{18 * time.Second: "18s", 4 * time.Minute: "4m", 2 * time.Hour: "2h", 72 * time.Hour: "3d"} { if got := compactDuration(duration); got != want { t.Fatalf("%v=%s want %s", duration, got, want) } } } func ptr(value string) *string { return &value } func TestShortenerShrinksToUniquePrefixes(t *testing.T) { events := []history.Event{ {RunID: "1", ChildID: "run", Repo: "a", Job: "push", Status: "success", Rev: "aa100000", Time: time.Now()}, {RunID: "2", ChildID: "run", Repo: "b", Job: "push", Status: "success", Rev: "aa200000", Time: time.Now()}, } short := buildShortener(events, nil, nil) view := runView{RunID: "1", Rev: "aa100000", FullRev: "aa100000", Pronounce: proquint.Encode(1)} short.apply(&view) if view.Rev != "aa10" || view.FullHead != "aa10" || view.FullTail != "0000" { t.Fatalf("rev shortening: %#v", view) } } func TestRunningPushPreservesParentIdentity(t *testing.T) { now := time.Now().UTC() id := fmt.Sprint(now.Add(-time.Second).UnixNano()) pending := []inbox.Pending{{ID: id, Phase: "preparing", Event: trigger.PushEvent{Kind: "push", Repo: "repo", New: "requested", Ref: "refs/heads/trunk"}}} active := []runstate.Active{{RunID: id, ChildID: "001", Repo: "repo", Job: "smoke", Rev: "abcdef123", Ref: "refs/heads/trunk", Trigger: "push", StartedAt: now.Add(-time.Second)}} dashboard := buildDashboard(now, nil, pending, active, false) if len(dashboard.Progress) != 1 || dashboard.Progress[0].Job != "push" || dashboard.Progress[0].Status != "running" || dashboard.Progress[0].Rev != "abcd" { t.Fatalf("progress=%#v", dashboard.Progress) } detail := buildRun(id, now, nil, pending, active) if detail.Parent.Job != "push" || detail.Parent.Status != "running" || detail.Parent.Rev != "abcd" || len(detail.Children) != 1 { t.Fatalf("detail=%#v", detail) } } func TestBuildRunPreservesLegacyChildren(t *testing.T) { now := time.Now().UTC() events := []history.Event{ {RunID: "old", Repo: "repo", Job: "check", Status: "failed", Detail: "boom", Time: now.Add(-time.Minute)}, {RunID: "old", Repo: "repo", Job: "test", Status: "success", Time: now}, } got := buildRun("old", now, events, nil, nil) if !got.Known || got.Parent.Job != "test" || len(got.Children) != 2 || got.Children[0].ChildID != "legacy-001" || got.Children[0].Detail != "boom" { t.Fatalf("legacy=%#v", got) } } func TestDashboardUsesRunIDAsOrderingTieBreaker(t *testing.T) { now := time.Now().UTC() events := []history.Event{{RunID: "a", ChildID: "run", Repo: "a", Status: "success", Time: now}, {RunID: "b", ChildID: "run", Repo: "b", Status: "success", Time: now}} view := buildDashboard(now, events, nil, nil, false) if view.Success[0].RunID != "b" { t.Fatalf("success order=%#v", view.Success) } pending := []inbox.Pending{{ID: "a", Phase: "queued", Event: trigger.PushEvent{Kind: "push", Repo: "a", New: "x", Ref: "r"}}, {ID: "b", Phase: "queued", Event: trigger.PushEvent{Kind: "push", Repo: "b", New: "x", Ref: "r"}}} view = buildDashboard(now, nil, pending, nil, false) if view.Progress[0].RunID != "b" { t.Fatalf("progress order=%#v", view.Progress) } }