generated from mathias/template-go-web
TDD: Job.Seconds() + StageSeconds() derive real CI/CD dwell time from the latest run's per-job created_at/updated_at (last job in pipeline order = deploy/stage 07, everything before it = CI/stage 06). Frontend: weightedSpineDist() redistributes the pixel-time-budget the 06/07 segments already had, splitting it by real CI:CD duration ratio instead of raw pixel width. Falls back to the exact prior constant-speed sweep when no run data is available (weights default to segment pixel length) or when a job is skipped (0 duration) — no behavior change for stages 00-05/08, which still have no live timing source (same gap as #5's ledger item). Verified: real duration values flow through /api/atlas.json (ci_duration_s: 18 observed against a real run), full page screenshot confirms no visual regression.
157 lines
5.1 KiB
Go
157 lines
5.1 KiB
Go
package atlas_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
|
)
|
|
|
|
func TestLatestRunJobs_GroupsNewestRunReversedToPipelineOrder(t *testing.T) {
|
|
// Gitea returns tasks newest-first (deploy finished last → appears first).
|
|
tasks := []byte(`{"workflow_runs":[
|
|
{"run_number":28,"name":"Deploy via GitOps","status":"success","head_sha":"633ba153f26abc","display_title":"feat: x"},
|
|
{"run_number":28,"name":"Build & Import","status":"success"},
|
|
{"run_number":28,"name":"Lint / Test / Vet","status":"success"},
|
|
{"run_number":27,"name":"Deploy via GitOps","status":"failure"}
|
|
]}`)
|
|
|
|
s, err := atlas.LatestRunJobs(tasks)
|
|
if err != nil {
|
|
t.Fatalf("LatestRunJobs: %v", err)
|
|
}
|
|
if s.Number != 28 || s.SHA != "633ba153f26abc" || s.Title != "feat: x" {
|
|
t.Fatalf("summary = %+v", s)
|
|
}
|
|
// only run 28's jobs, reversed to pipeline order (Lint → Build → Deploy)
|
|
got := []string{}
|
|
for _, j := range s.Jobs {
|
|
got = append(got, j.Name)
|
|
}
|
|
want := []string{"Lint / Test / Vet", "Build & Import", "Deploy via GitOps"}
|
|
if len(got) != 3 || got[0] != want[0] || got[2] != want[2] {
|
|
t.Fatalf("jobs = %v, want %v", got, want)
|
|
}
|
|
if s.State() != "success" {
|
|
t.Fatalf("state = %q, want success", s.State())
|
|
}
|
|
}
|
|
|
|
func TestRunSummary_StateFailsIfAnyJobFailed(t *testing.T) {
|
|
s := atlas.RunSummary{Jobs: []atlas.Job{
|
|
{Status: "success"}, {Status: "completed", Conclusion: "failure"},
|
|
}}
|
|
if s.State() != "failure" {
|
|
t.Fatalf("state = %q, want failure", s.State())
|
|
}
|
|
}
|
|
|
|
func TestRunNodes_SummaryThenPerJobColoured(t *testing.T) {
|
|
s := atlas.RunSummary{Number: 28, SHA: "633ba153f26", Title: "feat: x", Jobs: []atlas.Job{
|
|
{Name: "Lint / Test / Vet", Status: "success"},
|
|
{Name: "Deploy via GitOps", Status: "failure"},
|
|
}}
|
|
nodes := atlas.RunNodes(s)
|
|
if len(nodes) != 3 {
|
|
t.Fatalf("want 3 nodes (summary + 2 jobs), got %d", len(nodes))
|
|
}
|
|
if nodes[0].Title != "▶ run #28 · failure" || nodes[0].Pill != "var(--coral)" {
|
|
t.Fatalf("summary node = %+v", nodes[0])
|
|
}
|
|
if nodes[1].Title != "Lint / Test / Vet" || nodes[1].Pill != "var(--green)" {
|
|
t.Fatalf("job node 1 = %+v", nodes[1])
|
|
}
|
|
if nodes[2].Pill != "var(--coral)" {
|
|
t.Fatalf("failed job pill = %q", nodes[2].Pill)
|
|
}
|
|
}
|
|
|
|
func TestRecentRuns_GroupsRunsNewestFirstWithAggregateState(t *testing.T) {
|
|
tasks := []byte(`{"workflow_runs":[
|
|
{"run_number":28,"name":"Deploy","status":"success"},
|
|
{"run_number":28,"name":"Build","status":"success"},
|
|
{"run_number":27,"name":"Deploy","status":"completed","conclusion":"failure"},
|
|
{"run_number":26,"name":"Build","status":"running"},
|
|
{"run_number":25,"name":"Deploy","status":"success"}
|
|
]}`)
|
|
|
|
dots, err := atlas.RecentRuns(tasks, 3)
|
|
if err != nil {
|
|
t.Fatalf("RecentRuns: %v", err)
|
|
}
|
|
if len(dots) != 3 {
|
|
t.Fatalf("want 3 dots (capped), got %d: %+v", len(dots), dots)
|
|
}
|
|
want := []atlas.RunDot{{28, "success"}, {27, "failure"}, {26, "running"}}
|
|
for i := range want {
|
|
if dots[i] != want[i] {
|
|
t.Fatalf("dot[%d] = %+v, want %+v", i, dots[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunState_SkippedJobsCountAsOK(t *testing.T) {
|
|
// A tag-push run skips the deploy job; the run still succeeded.
|
|
s := atlas.RunSummary{Jobs: []atlas.Job{
|
|
{Status: "skipped"}, {Status: "success"}, {Status: "success"},
|
|
}}
|
|
if s.State() != "success" {
|
|
t.Fatalf("skipped+success run state = %q, want success", s.State())
|
|
}
|
|
}
|
|
|
|
func TestLatestRunJobs_ErrorsWhenEmpty(t *testing.T) {
|
|
if _, err := atlas.LatestRunJobs([]byte(`{"workflow_runs":[]}`)); err == nil {
|
|
t.Fatal("expected error on empty, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLatestRunJobs_ParsesPerJobTimestampsIntoSeconds(t *testing.T) {
|
|
tasks := []byte(`{"workflow_runs":[
|
|
{"run_number":28,"name":"Deploy via GitOps","status":"success","created_at":"2026-07-20T21:05:44Z","updated_at":"2026-07-20T21:05:50Z"},
|
|
{"run_number":28,"name":"Lint / Test / Vet","status":"success","created_at":"2026-07-20T21:05:39Z","updated_at":"2026-07-20T21:05:44Z"}
|
|
]}`)
|
|
|
|
s, err := atlas.LatestRunJobs(tasks)
|
|
if err != nil {
|
|
t.Fatalf("LatestRunJobs: %v", err)
|
|
}
|
|
// pipeline order: Lint first, Deploy last
|
|
if got := s.Jobs[0].Seconds(); got != 5 {
|
|
t.Fatalf("Lint job seconds = %v, want 5", got)
|
|
}
|
|
if got := s.Jobs[1].Seconds(); got != 6 {
|
|
t.Fatalf("Deploy job seconds = %v, want 6", got)
|
|
}
|
|
}
|
|
|
|
func TestStageSeconds_LastJobIsDeploySumOfRestIsCI(t *testing.T) {
|
|
s := atlas.RunSummary{Jobs: []atlas.Job{
|
|
{Name: "Lint", Started: mustParse("2026-07-20T21:00:00Z"), Finished: mustParse("2026-07-20T21:00:10Z")}, // 10s
|
|
{Name: "Build", Started: mustParse("2026-07-20T21:00:10Z"), Finished: mustParse("2026-07-20T21:00:25Z")}, // 15s
|
|
{Name: "Deploy", Started: mustParse("2026-07-20T21:00:25Z"), Finished: mustParse("2026-07-20T21:00:33Z")}, // 8s
|
|
}}
|
|
ci, cd := atlas.StageSeconds(s)
|
|
if ci != 25 {
|
|
t.Fatalf("ci = %v, want 25", ci)
|
|
}
|
|
if cd != 8 {
|
|
t.Fatalf("cd = %v, want 8", cd)
|
|
}
|
|
}
|
|
|
|
func TestStageSeconds_NoJobsReturnsZero(t *testing.T) {
|
|
ci, cd := atlas.StageSeconds(atlas.RunSummary{})
|
|
if ci != 0 || cd != 0 {
|
|
t.Fatalf("ci=%v cd=%v, want 0,0", ci, cd)
|
|
}
|
|
}
|
|
|
|
func mustParse(s string) time.Time {
|
|
t, err := time.Parse(time.RFC3339, s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|