generated from mathias/template-go-web
feat(atlas): weight replay pacing on CI/CD stages by real job durations (#5)
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.
This commit is contained in:
@@ -2,6 +2,7 @@ package atlas_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
||||
)
|
||||
@@ -104,3 +105,52 @@ func TestLatestRunJobs_ErrorsWhenEmpty(t *testing.T) {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user