Commit b953e91

Eric Bower  ·  2026-08-18 11:51:33 -0400 EDT
parent a0e7733
fix: order previously ran tasks by start time
4 files changed,  +63, -8
+11, -3
......@@ -74,18 +74,26 @@ func newPipelineUIState(repo, branch, commit, jobID string, stats *RepoStats) *P
7474 if stats != nil {
7575 ui.PredictedTotal = time.Duration(stats.AvgWallDurationMs) * time.Millisecond
7676
77- // Pre-populate expected tasks in deterministic order
77+ // Pre-populate expected tasks sorted by start time
7878 var names []string
7979 for name := range stats.Tasks {
8080 names = append(names, name)
8181 }
82- sort.Strings(names)
82+ sort.Slice(names, func(i, j int) bool {
83+ ti := stats.Tasks[names[i]]
84+ tj := stats.Tasks[names[j]]
85+ if ti.LastStartedAt != tj.LastStartedAt {
86+ return ti.LastStartedAt < tj.LastStartedAt
87+ }
88+ return names[i] < names[j]
89+ })
8390
8491 for _, name := range names {
8592 ts := stats.Tasks[name]
8693 ui.Tasks[name] = &TaskUIState{
8794 Name: name,
8895 Status: "expected",
96+ Created: ts.LastStartedAt,
8997 PredictedDur: time.Duration(ts.AvgDurationMs) * time.Millisecond,
9098 PrevStatus: ts.LastStatus,
9199 PrevExitCode: ts.LastExitCode,
......@@ -864,7 +872,7 @@ func waitAndReportUI(cfg *Cfg, log *slog.Logger, repoName, jobID, domainOrEvent,
864872 ended, _ := strconv.ParseInt(s.Ended, 10, 64)
865873 dur := time.Duration(ended-created) * time.Second
866874 code, _ := strconv.Atoi(s.ExitCode)
867- updateTaskStats(stats, s.Short, dur, code)
875+ updateTaskStats(stats, s.Short, dur, code, created)
868876 }
869877 updateRepoStats(stats, ctrl.ui.Elapsed, overallStatus)
870878 _ = saveRepoStats(stats)
+36, -0
......@@ -48,6 +48,42 @@ func TestUI_ForecastStateInitialization(t *testing.T) {
4848 }
4949 }
5050
51+func TestUI_ForecastStateSortByStartTime(t *testing.T) {
52+ // Tasks defined out of chronological order:
53+ // "test" started first at t=100
54+ // "lint" started second at t=200
55+ // "fmt" started third at t=300
56+ stats := &RepoStats{
57+ Repo: "my-repo",
58+ Tasks: map[string]*TaskStats{
59+ "fmt": {
60+ AvgDurationMs: 850,
61+ LastStartedAt: 300,
62+ },
63+ "lint": {
64+ AvgDurationMs: 4200,
65+ LastStartedAt: 200,
66+ },
67+ "test": {
68+ AvgDurationMs: 13500,
69+ LastStartedAt: 100,
70+ },
71+ },
72+ }
73+
74+ ui := newPipelineUIState("my-repo", "main", "8f2a1b9", "job123", stats)
75+
76+ expectedOrder := []string{"test", "lint", "fmt"}
77+ if len(ui.TaskOrder) != len(expectedOrder) {
78+ t.Fatalf("expected %d tasks in order, got %d", len(expectedOrder), len(ui.TaskOrder))
79+ }
80+ for i, expected := range expectedOrder {
81+ if ui.TaskOrder[i] != expected {
82+ t.Errorf("expected task at position %d to be %q, got %q (order: %v)", i, expected, ui.TaskOrder[i], ui.TaskOrder)
83+ }
84+ }
85+}
86+
5187 func TestUI_RenderProgressBar(t *testing.T) {
5288 tests := []struct {
5389 elapsed time.Duration
+5, -1
......@@ -26,6 +26,7 @@ type TaskStats struct {
2626 LastExitCode int `json:"last_exit_code"`
2727 FailureStreak int `json:"failure_streak,omitempty"`
2828 SeenCount int `json:"seen_count"`
29+ LastStartedAt int64 `json:"last_started_at,omitempty"`
2930 }
3031
3132 // getStatsPath returns the path to stats.json for the specified repository.
......@@ -86,7 +87,7 @@ func saveRepoStats(stats *RepoStats) error {
8687 }
8788
8889 // updateTaskStats updates single task statistics using EMA duration (alpha = 0.3).
89-func updateTaskStats(stats *RepoStats, name string, actualDur time.Duration, exitCode int) {
90+func updateTaskStats(stats *RepoStats, name string, actualDur time.Duration, exitCode int, startedAt ...int64) {
9091 if stats.Tasks == nil {
9192 stats.Tasks = make(map[string]*TaskStats)
9293 }
......@@ -107,6 +108,9 @@ func updateTaskStats(stats *RepoStats, name string, actualDur time.Duration, exi
107108 task.LastDurationMs = actualMs
108109 task.LastExitCode = exitCode
109110 task.SeenCount++
111+ if len(startedAt) > 0 && startedAt[0] > 0 {
112+ task.LastStartedAt = startedAt[0]
113+ }
110114
111115 if exitCode == 0 {
112116 task.LastStatus = "success"
+11, -4
......@@ -55,15 +55,18 @@ func TestStats_EMACalculation(t *testing.T) {
5555 t.Errorf("expected FailureStreak 1, got %d", lintTask.FailureStreak)
5656 }
5757
58- // Fail again -> streak increments
59- updateTaskStats(stats, "lint", 4100*time.Millisecond, 1)
58+ // Fail again with startedAt -> streak increments and LastStartedAt updated
59+ updateTaskStats(stats, "lint", 4100*time.Millisecond, 1, 1700000500)
6060 lintTask = stats.Tasks["lint"]
6161 if lintTask.FailureStreak != 2 {
6262 t.Errorf("expected FailureStreak 2, got %d", lintTask.FailureStreak)
6363 }
64+ if lintTask.LastStartedAt != 1700000500 {
65+ t.Errorf("expected LastStartedAt 1700000500, got %d", lintTask.LastStartedAt)
66+ }
6467
6568 // Succeed -> streak resets to 0
66- updateTaskStats(stats, "lint", 3900*time.Millisecond, 0)
69+ updateTaskStats(stats, "lint", 3900*time.Millisecond, 0, 1700000600)
6770 lintTask = stats.Tasks["lint"]
6871 if lintTask.FailureStreak != 0 {
6972 t.Errorf("expected FailureStreak reset to 0, got %d", lintTask.FailureStreak)
......@@ -71,6 +74,9 @@ func TestStats_EMACalculation(t *testing.T) {
7174 if lintTask.LastStatus != "success" {
7275 t.Errorf("expected LastStatus success, got %s", lintTask.LastStatus)
7376 }
77+ if lintTask.LastStartedAt != 1700000600 {
78+ t.Errorf("expected LastStartedAt 1700000600, got %d", lintTask.LastStartedAt)
79+ }
7480 }
7581
7682 func TestStats_StorageAndPath(t *testing.T) {
......@@ -98,6 +104,7 @@ func TestStats_StorageAndPath(t *testing.T) {
98104 LastStatus: "success",
99105 LastExitCode: 0,
100106 SeenCount: 5,
107+ LastStartedAt: 1700000000,
101108 },
102109 },
103110 }
......@@ -113,7 +120,7 @@ func TestStats_StorageAndPath(t *testing.T) {
113120 if loaded.Repo != repo || loaded.RunsCount != 5 || loaded.AvgWallDurationMs != 12000 {
114121 t.Errorf("loaded stats mismatch: %+v", loaded)
115122 }
116- if task, ok := loaded.Tasks["build"]; !ok || task.AvgDurationMs != 6000 {
123+ if task, ok := loaded.Tasks["build"]; !ok || task.AvgDurationMs != 6000 || task.LastStartedAt != 1700000000 {
117124 t.Errorf("loaded task stats mismatch: %+v", task)
118125 }
119126 }