Repository: akuity/kargo
Pull request #2106 (2024-06-04 15:52:51)

chore(backport release-0.7): feat: (api/ui) Sort promotions by phase and name
Automated backport to `release-0.7`, triggered by a label in #1949.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/akuity/kargo/pull/2106?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) Report
Attention: Patch coverage is `91.66667%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 45.91%. Comparing base [(`23ead83`)](https://app.codecov.io/gh/akuity/kargo/commit/23ead83957f36dff4b9e2c9b6544d35ef6750b78?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) to head [(`e2980fc`)](https://app.codecov.io/gh/akuity/kargo/commit/e2980fc9a3dc5944d5673e6982298ebf06ac7fcc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).

| [Files](https://app.codecov.io/gh/akuity/kargo/pull/2106?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) | Patch % | Lines |
|---|---|---|
| [api/v1alpha1/promotion\_helpers.go](https://app.codecov.io/gh/akuity/kargo/pull/2106?src=pr&el=tree&filepath=api%2Fv1alpha1%2Fpromotion_helpers.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-YXBpL3YxYWxwaGExL3Byb21vdGlvbl9oZWxwZXJzLmdv) | 90.90% | [2 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/2106?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff               @@
##           release-0.7    #2106      +/-   ##
===============================================
+ Coverage        45.88%   45.91%   +0.02%     
===============================================
  Files              238      238              
  Lines            16551    16549       -2     
===============================================
+ Hits              7595     7599       +4     
+ Misses            8584     8578       -6     
  Partials           372      372              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/akuity/kargo/pull/2106?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).


Patch:
diff --git a/api/v1alpha1/promotion_helpers.go b/api/v1alpha1/promotion_helpers.go
index e0604bb4..f4ab5e68 100644
--- a/api/v1alpha1/promotion_helpers.go
+++ b/api/v1alpha1/promotion_helpers.go
@@ -3,6 +3,7 @@ package v1alpha1
 import (
 	"context"
 	"fmt"
+	"strings"
 	"time"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -53,3 +54,65 @@ func RefreshPromotion(
 	}
 	return promo, nil
 }
+
+// ComparePromotionByPhaseAndCreationTime compares two Promotions by their
+// phase and creation timestamp. It returns a negative value if Promotion `a`
+// should come before Promotion `b`, a positive value if Promotion `a` should
+// come after Promotion `b`, or zero if they are considered equal for sorting
+// purposes. It can be used in conjunction with slices.SortFunc to sort a list
+// of Promotions.
+//
+// The order of Promotions is as follows:
+//  1. Running Promotions
+//  2. Non-terminal Promotions (ordered by ULID in ascending order)
+//  3. Terminal Promotions (ordered by ULID in descending order)
+func ComparePromotionByPhaseAndCreationTime(a, b Promotion) int {
+	// Compare the phases of the Promotions first.
+	if phaseCompare := ComparePromotionPhase(a.Status.Phase, b.Status.Phase); phaseCompare != 0 {
+		return phaseCompare
+	}
+
+	switch {
+	case !a.Status.Phase.IsTerminal():
+		// Non-terminal Promotions are ordered in ascending order based on the
+		// ULID in the Promotion name. This ensures that the Promotion which
+		// was (or will be) enqueued first is at the top.
+		return strings.Compare(a.Name, b.Name)
+	default:
+		// Terminal Promotions are ordered in descending order based on the
+		// ULID in the Promotion name. This ensures that the most recent
+		// Promotion is at the top, limiting the number of Promotions which
+		// have to be further inspected to collect the "new" Promotions.
+		return strings.Compare(b.Name, a.Name)
+	}
+}
+
+// ComparePromotionPhase compares two Promotion phases. It returns a negative
+// value if phase `a` should come before phase `b`, a positive value if phase
+// `a` should come after phase `b`, or zero if they are considered equal for
+// sorting purposes. It can be used in combination with slices.SortFunc to sort
+// a list of Promotion phases.
+//
+// The order of Promotion phases is as follows:
+//  1. Running
+//  2. Non-terminal phases
+//  3. Terminal phases
+func ComparePromotionPhase(a, b PromotionPhase) int {
+	aRunning, bRunning := a == PromotionPhaseRunning, b == PromotionPhaseRunning
+	aTerminal, bTerminal := a.IsTerminal(), b.IsTerminal()
+
+	// NB: The order of the cases here is important, as "Running" is a special
+	// case that should always come before any other phase.
+	switch {
+	case aRunning && !bRunning:
+		return -1
+	case !aRunning && bRunning:
+		return 1
+	case !aTerminal && bTerminal:
+		return -1
+	case aTerminal && !bTerminal:
+		return 1
+	default:
+		return 0
+	}
+}
diff --git a/internal/api/list_promotions_v1alpha1.go b/internal/api/list_promotions_v1alpha1.go
index 954d1321..3d43e725 100644
--- a/internal/api/list_promotions_v1alpha1.go
+++ b/internal/api/list_promotions_v1alpha1.go
@@ -3,7 +3,7 @@ package api
 import (
 	"context"
 	"fmt"
-	"sort"
+	"slices"
 
 	"connectrpc.com/connect"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -39,9 +39,7 @@ func (s *server) ListPromotions(
 		return nil, fmt.Errorf("list promotions: %w", err)
 	}
 
-	sort.Slice(list.Items, func(i, j int) bool {
-		return list.Items[i].CreationTimestamp.After(list.Items[j].CreationTimestamp.Time)
-	})
+	slices.SortFunc(list.Items, kargoapi.ComparePromotionByPhaseAndCreationTime)
 
 	promotions := make([]*kargoapi.Promotion, len(list.Items))
 	for idx := range list.Items {
diff --git a/internal/controller/stages/stages.go b/internal/controller/stages/stages.go
index 4ef3bcc4..0d473757 100644
--- a/internal/controller/stages/stages.go
+++ b/internal/controller/stages/stages.go
@@ -1052,7 +1052,7 @@ func (r *reconciler) syncPromotions(
 
 	// Sort the Promotions by phase and creation time so that we can determine the
 	// current state of the Stage.
-	slices.SortFunc(promotions, comparePromotionByPhaseAndCreationTime)
+	slices.SortFunc(promotions, kargoapi.ComparePromotionByPhaseAndCreationTime)
 
 	// If the latest Promotion is in a running phase, the Stage is promoting.
 	if latestPromo := promotions[0]; !latestPromo.Status.Phase.IsTerminal() {
@@ -1673,63 +1673,3 @@ func (r *reconciler) recordFreightVerificationEvent(
 
 	r.recorder.AnnotatedEventf(fr, annotations, corev1.EventTypeNormal, reason, message)
 }
-
-// comparePromotionByPhaseAndCreationTime compares two Promotions by their
-// phase and creation timestamp. It returns a negative value if Promotion `a`
-// should come before Promotion `b`, a positive value if Promotion `a` should
-// come after Promotion `b`, or zero if they are considered equal for sorting
-// purposes.
-//
-// The order of Promotions is as follows:
-//  1. Running, ordered by ULID in ascending order.
-//  2. Terminated, ordered by ULID in ascending order.
-//  3. Non-terminal, ordered by ULID in descending order.
-func comparePromotionByPhaseAndCreationTime(a, b kargoapi.Promotion) int {
-	// Compare the phases of the Promotions first.
-	if phaseCompare := comparePromotionPhase(a.Status.Phase, b.Status.Phase); phaseCompare != 0 {
-		return phaseCompare
-	}
-
-	switch {
-	case !a.Status.Phase.IsTerminal():
-		// Non-terminal Promotions are ordered in ascending order based on the
-		// ULID in the Promotion name. This ensures that the Promotion which
-		// was (or will be) enqueued first is at the top.
-		return strings.Compare(a.Name, b.Name)
-	default:
-		// Terminal Promotions are ordered in descending order based on the
-		// ULID in the Promotion name. This ensures that the most recent
-		// Promotion is at the top, limiting the number of Promotions which
-		// have to be further inspected to collect the "new" Promotions.
-		return strings.Compare(b.Name, a.Name)
-	}
-}
-
-// comparePromotionPhase compares two Promotion phases. It returns a negative
-// value if phase `a` should come before phase `b`, a positive value if phase
-// `a` should come after phase `b`, or zero if they are considered equal for
-// sorting purposes.
-//
-// The order of Promotion phases is as follows:
-//  1. Running
-//  2. Non-terminal phases
-//  3. Terminal phases
-func comparePromotionPhase(a, b kargoapi.PromotionPhase) int {
-	aRunning, bRunning := a == kargoapi.PromotionPhaseRunning, b == kargoapi.PromotionPhaseRunning
-	aTerminal, bTerminal := a.IsTerminal(), b.IsTerminal()
-
-	// NB: The order of the cases here is important, as "Running" is a special
-	// case that should always come before any other phase.
-	switch {
-	case aRunning && !bRunning:
-		return -1
-	case !aRunning && bRunning:
-		return 1
-	case !aTerminal && bTerminal:
-		return -1
-	case aTerminal && !bTerminal:
-		return 1
-	default:
-		return 0
-	}
-}
diff --git a/ui/src/features/stage/promotions.tsx b/ui/src/features/stage/promotions.tsx
index 5b882038..f69401ff 100644
--- a/ui/src/features/stage/promotions.tsx
+++ b/ui/src/features/stage/promotions.tsx
@@ -24,8 +24,6 @@ import { KargoService } from '@ui/gen/service/v1alpha1/service_connect';
 import { ListPromotionsResponse } from '@ui/gen/service/v1alpha1/service_pb';
 import { Freight, Promotion } from '@ui/gen/v1alpha1/generated_pb';
 
-import { sortPromotions } from './utils/sort';
-
 export const Promotions = () => {
   const client = useQueryClient();
   const { name: projectName, stageName } = useParams();
@@ -95,7 +93,7 @@ export const Promotions = () => {
 
   const promotions = React.useMemo(() => {
     // Immutable sorting
-    return [...(promotionsResponse?.promotions || [])].sort(sortPromotions);
+    return [...(promotionsResponse?.promotions || [])];
   }, [promotionsResponse]);
 
   const columns: ColumnsType<Promotion> = [
diff --git a/ui/src/features/stage/utils/sort.ts b/ui/src/features/stage/utils/sort.ts
deleted file mode 100644
index fecc40ea..00000000
--- a/ui/src/features/stage/utils/sort.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Promotion } from '@ui/gen/v1alpha1/generated_pb';
-
-export const sortPromotions = (a: Promotion, b: Promotion) => {
-  const timestampDiff =
-    Number(b.metadata?.creationTimestamp?.seconds || 0) -
-    Number(a.metadata?.creationTimestamp?.seconds || 0);
-
-  const aIsRunning = a.status?.phase === 'Running';
-  const bIsRunning = b.status?.phase === 'Running';
-  const aIsPending = a.status?.phase === 'Pending';
-  const bIsPending = b.status?.phase === 'Pending';
-
-  if (aIsRunning || bIsRunning) {
-    if (aIsRunning && bIsRunning) {
-      return timestampDiff;
-    }
-    return aIsRunning ? -1 : 1;
-  }
-
-  if (aIsPending || bIsPending) {
-    if (aIsPending && bIsPending) {
-      return timestampDiff;
-    }
-    return aIsPending ? -1 : 1;
-  }
-
-  return timestampDiff;
-};


Test patch:
diff --git a/api/v1alpha1/promotion_helpers_test.go b/api/v1alpha1/promotion_helpers_test.go
index 2a457479..ec83def7 100644
--- a/api/v1alpha1/promotion_helpers_test.go
+++ b/api/v1alpha1/promotion_helpers_test.go
@@ -3,7 +3,9 @@ package v1alpha1
 import (
 	"context"
 	"testing"
+	"time"
 
+	"github.com/oklog/ulid/v2"
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	k8sruntime "k8s.io/apimachinery/pkg/runtime"
@@ -62,3 +64,224 @@ func TestGetPromotion(t *testing.T) {
 		})
 	}
 }
+
+func Test_ComparePromotionByPhaseAndCreationTime(t *testing.T) {
+	now := time.Date(2024, time.April, 10, 0, 0, 0, 0, time.UTC)
+	ulidEarlier := ulid.MustNew(ulid.Timestamp(now.Add(-time.Hour)), nil)
+	ulidLater := ulid.MustNew(ulid.Timestamp(now.Add(time.Hour)), nil)
+
+	tests := []struct {
+		name     string
+		a        Promotion
+		b        Promotion
+		expected int
+	}{
+		{
+			name: "Running before Terminated",
+			a: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhaseRunning,
+				},
+			},
+			b: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhaseSucceeded,
+				},
+			},
+			expected: -1,
+		},
+		{
+			name: "Pending before Terminated",
+			a: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhasePending,
+				},
+			},
+			b: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhaseSucceeded,
+				},
+			},
+			expected: -1,
+		},
+		{
+			name: "Pending after Running",
+			a: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhasePending,
+				},
+			},
+			b: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhaseRunning,
+				},
+			},
+			expected: 1,
+		},
+		{
+			name: "Terminated after Running",
+			a: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhaseFailed,
+				},
+			},
+			b: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhaseRunning,
+				},
+			},
+			expected: 1,
+		},
+		{
+			name: "Earlier ULID first if both Running",
+			a: Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "promotion." + ulidEarlier.String(),
+				},
+				Status: PromotionStatus{
+					Phase: PromotionPhaseRunning,
+				},
+			},
+			b: Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "promotion." + ulidLater.String(),
+				},
+				Status: PromotionStatus{
+					Phase: PromotionPhaseRunning,
+				},
+			},
+			expected: -1,
+		},
+		{
+			name: "Later ULID first if both Terminated",
+			a: Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "promotion." + ulidLater.String(),
+				},
+				Status: PromotionStatus{
+					Phase: PromotionPhaseErrored,
+				},
+			},
+			b: Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "promotion." + ulidEarlier.String(),
+				},
+				Status: PromotionStatus{
+					Phase: PromotionPhaseSucceeded,
+				},
+			},
+			expected: -1,
+		},
+		{
+			name: "Equal promotions",
+			a: Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:              "promotion-a",
+					CreationTimestamp: metav1.Time{Time: now},
+				},
+				Status: PromotionStatus{
+					Phase: PromotionPhasePending,
+				},
+			},
+			b: Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:              "promotion-a",
+					CreationTimestamp: metav1.Time{Time: now},
+				},
+				Status: PromotionStatus{
+					Phase: PromotionPhasePending,
+				},
+			},
+			expected: 0,
+		},
+		{
+			name: "Nil creation timestamps",
+			a: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhasePending,
+				},
+			},
+			b: Promotion{
+				Status: PromotionStatus{
+					Phase: PromotionPhasePending,
+				},
+			},
+			expected: 0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := ComparePromotionByPhaseAndCreationTime(tt.a, tt.b)
+			require.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestComparePromotionPhase(t *testing.T) {
+	tests := []struct {
+		name     string
+		a        PromotionPhase
+		b        PromotionPhase
+		expected int
+	}{
+		{
+			name:     "Running before Terminated",
+			a:        PromotionPhaseRunning,
+			b:        PromotionPhaseSucceeded,
+			expected: -1,
+		},
+		{
+			name:     "Terminated after Running",
+			a:        PromotionPhaseFailed,
+			b:        PromotionPhaseRunning,
+			expected: 1,
+		},
+		{
+			name:     "Running before other phase",
+			a:        PromotionPhaseRunning,
+			b:        PromotionPhasePending,
+			expected: -1,
+		},
+		{
+			name:     "Other phase after Running",
+			a:        "",
+			b:        PromotionPhaseRunning,
+			expected: 1,
+		},
+		{
+			name:     "Pending before Terminated",
+			a:        PromotionPhasePending,
+			b:        PromotionPhaseErrored,
+			expected: -1,
+		},
+		{
+			name:     "Pending after Running",
+			a:        PromotionPhasePending,
+			b:        PromotionPhaseRunning,
+			expected: 1,
+		},
+		{
+			name:     "Equal Running phases",
+			a:        PromotionPhaseRunning,
+			b:        PromotionPhaseRunning,
+			expected: 0,
+		},
+		{
+			name: "Equal Terminated phases",
+			a:    PromotionPhaseSucceeded,
+			b:    PromotionPhaseFailed,
+		},
+		{
+			name:     "Equal other phases",
+			a:        PromotionPhasePending,
+			b:        PromotionPhasePending,
+			expected: 0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			require.Equal(t, tt.expected, ComparePromotionPhase(tt.a, tt.b))
+		})
+	}
+}
diff --git a/internal/api/list_promotions_v1alpha1_test.go b/internal/api/list_promotions_v1alpha1_test.go
index 60773c2b..d5034bb2 100644
--- a/internal/api/list_promotions_v1alpha1_test.go
+++ b/internal/api/list_promotions_v1alpha1_test.go
@@ -2,10 +2,11 @@ package api
 
 import (
 	"context"
+	"fmt"
 	"testing"
-	"time"
 
 	"connectrpc.com/connect"
+	"github.com/oklog/ulid/v2"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -23,6 +24,12 @@ import (
 )
 
 func TestListPromotions(t *testing.T) {
+	// We need some promotion names with ULIDs to test the sorting.
+	oldestPromotionName := fmt.Sprintf("some-stage.%s.oldest", ulid.Make())
+	olderPromotionName := fmt.Sprintf("some-stage.%s.older", ulid.Make())
+	oldPromotionName := fmt.Sprintf("some-stage.%s.old", ulid.Make())
+	newPromotionName := fmt.Sprintf("some-stage.%s.new", ulid.Make())
+
 	testCases := map[string]struct {
 		req        *svcv1alpha1.ListPromotionsRequest
 		objects    []client.Object
@@ -67,7 +74,7 @@ func TestListPromotions(t *testing.T) {
 				require.Nil(t, r)
 			},
 		},
-		"orders by creation time": {
+		"orders by ULID and phase": {
 			req: &svcv1alpha1.ListPromotionsRequest{
 				Project: "kargo-demo",
 			},
@@ -75,30 +82,38 @@ func TestListPromotions(t *testing.T) {
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 				&kargoapi.Promotion{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:              "oldest-promotion",
-						Namespace:         "kargo-demo",
-						CreationTimestamp: metav1.NewTime(metav1.Now().Add(-24 * time.Hour)),
+						Name:      oldestPromotionName,
+						Namespace: "kargo-demo",
+					},
+					Status: kargoapi.PromotionStatus{
+						Phase: kargoapi.PromotionPhaseRunning,
 					},
 				},
 				&kargoapi.Promotion{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:              "new-promotion",
-						Namespace:         "kargo-demo",
-						CreationTimestamp: metav1.Now(),
+						Name:      newPromotionName,
+						Namespace: "kargo-demo",
+					},
+					Status: kargoapi.PromotionStatus{
+						Phase: kargoapi.PromotionPhaseSucceeded,
 					},
 				},
 				&kargoapi.Promotion{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:              "older-promotion",
-						Namespace:         "kargo-demo",
-						CreationTimestamp: metav1.NewTime(metav1.Now().Add(-2 * time.Hour)),
+						Name:      olderPromotionName,
+						Namespace: "kargo-demo",
+					},
+					Status: kargoapi.PromotionStatus{
+						Phase: kargoapi.PromotionPhaseFailed,
 					},
 				},
 				&kargoapi.Promotion{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:              "old-promotion",
-						Namespace:         "kargo-demo",
-						CreationTimestamp: metav1.NewTime(metav1.Now().Add(-1 * time.Hour)),
+						Name:      oldPromotionName,
+						Namespace: "kargo-demo",
+					},
+					Status: kargoapi.PromotionStatus{
+						Phase: kargoapi.PromotionPhasePending,
 					},
 				},
 			},
@@ -107,11 +122,11 @@ func TestListPromotions(t *testing.T) {
 				require.NotNil(t, r)
 				require.Len(t, r.Msg.GetPromotions(), 4)
 
-				// Check that the analysis templates are ordered by time.
-				require.Equal(t, "new-promotion", r.Msg.GetPromotions()[0].GetName())
-				require.Equal(t, "old-promotion", r.Msg.GetPromotions()[1].GetName())
-				require.Equal(t, "older-promotion", r.Msg.GetPromotions()[2].GetName())
-				require.Equal(t, "oldest-promotion", r.Msg.GetPromotions()[3].GetName())
+				// Check that the analysis templates are ordered by ULID and phase.
+				require.Equal(t, oldestPromotionName, r.Msg.GetPromotions()[0].GetName())
+				require.Equal(t, oldPromotionName, r.Msg.GetPromotions()[1].GetName())
+				require.Equal(t, newPromotionName, r.Msg.GetPromotions()[2].GetName())
+				require.Equal(t, olderPromotionName, r.Msg.GetPromotions()[3].GetName())
 			},
 		},
 	}
diff --git a/internal/controller/stages/stages_test.go b/internal/controller/stages/stages_test.go
index 81f72982..aefde2b6 100644
--- a/internal/controller/stages/stages_test.go
+++ b/internal/controller/stages/stages_test.go
@@ -3198,226 +3198,6 @@ func TestGetLatestVerifiedFreight(t *testing.T) {
 	}
 }
 
-func Test_comparePromotionByPhaseAndCreationTime(t *testing.T) {
-	now := fakeNow()
-	ulidEarlier := ulid.MustNew(ulid.Timestamp(now.Add(-time.Hour)), nil)
-	ulidLater := ulid.MustNew(ulid.Timestamp(now.Add(time.Hour)), nil)
-
-	tests := []struct {
-		name     string
-		a        kargoapi.Promotion
-		b        kargoapi.Promotion
-		expected int
-	}{
-		{
-			name: "Running before Terminated",
-			a: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseRunning,
-				},
-			},
-			b: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseSucceeded,
-				},
-			},
-			expected: -1,
-		},
-		{
-			name: "Pending before Terminated",
-			a: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhasePending,
-				},
-			},
-			b: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseSucceeded,
-				},
-			},
-			expected: -1,
-		},
-		{
-			name: "Pending after Running",
-			a: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhasePending,
-				},
-			},
-			b: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseRunning,
-				},
-			},
-			expected: 1,
-		},
-		{
-			name: "Terminated after Running",
-			a: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseFailed,
-				},
-			},
-			b: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseRunning,
-				},
-			},
-			expected: 1,
-		},
-		{
-			name: "Earlier ULID first if both Running",
-			a: kargoapi.Promotion{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "promotion." + ulidEarlier.String(),
-				},
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseRunning,
-				},
-			},
-			b: kargoapi.Promotion{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "promotion." + ulidLater.String(),
-				},
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseRunning,
-				},
-			},
-			expected: -1,
-		},
-		{
-			name: "Later ULID first if both Terminated",
-			a: kargoapi.Promotion{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "promotion." + ulidLater.String(),
-				},
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseErrored,
-				},
-			},
-			b: kargoapi.Promotion{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "promotion." + ulidEarlier.String(),
-				},
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhaseSucceeded,
-				},
-			},
-			expected: -1,
-		},
-		{
-			name: "Equal promotions",
-			a: kargoapi.Promotion{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:              "promotion-a",
-					CreationTimestamp: metav1.Time{Time: now},
-				},
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhasePending,
-				},
-			},
-			b: kargoapi.Promotion{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:              "promotion-a",
-					CreationTimestamp: metav1.Time{Time: now},
-				},
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhasePending,
-				},
-			},
-			expected: 0,
-		},
-		{
-			name: "Nil creation timestamps",
-			a: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhasePending,
-				},
-			},
-			b: kargoapi.Promotion{
-				Status: kargoapi.PromotionStatus{
-					Phase: kargoapi.PromotionPhasePending,
-				},
-			},
-			expected: 0,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			result := comparePromotionByPhaseAndCreationTime(tt.a, tt.b)
-			require.Equal(t, tt.expected, result)
-		})
-	}
-}
-func Test_comparePromotionPhase(t *testing.T) {
-	tests := []struct {
-		name     string
-		a        kargoapi.PromotionPhase
-		b        kargoapi.PromotionPhase
-		expected int
-	}{
-		{
-			name:     "Running before Terminated",
-			a:        kargoapi.PromotionPhaseRunning,
-			b:        kargoapi.PromotionPhaseSucceeded,
-			expected: -1,
-		},
-		{
-			name:     "Terminated after Running",
-			a:        kargoapi.PromotionPhaseFailed,
-			b:        kargoapi.PromotionPhaseRunning,
-			expected: 1,
-		},
-		{
-			name:     "Running before other phase",
-			a:        kargoapi.PromotionPhaseRunning,
-			b:        kargoapi.PromotionPhasePending,
-			expected: -1,
-		},
-		{
-			name:     "Other phase after Running",
-			a:        "",
-			b:        kargoapi.PromotionPhaseRunning,
-			expected: 1,
-		},
-		{
-			name:     "Pending before Terminated",
-			a:        kargoapi.PromotionPhasePending,
-			b:        kargoapi.PromotionPhaseErrored,
-			expected: -1,
-		},
-		{
-			name:     "Pending after Running",
-			a:        kargoapi.PromotionPhasePending,
-			b:        kargoapi.PromotionPhaseRunning,
-			expected: 1,
-		},
-		{
-			name:     "Equal Running phases",
-			a:        kargoapi.PromotionPhaseRunning,
-			b:        kargoapi.PromotionPhaseRunning,
-			expected: 0,
-		},
-		{
-			name: "Equal Terminated phases",
-			a:    kargoapi.PromotionPhaseSucceeded,
-			b:    kargoapi.PromotionPhaseFailed,
-		},
-		{
-			name:     "Equal other phases",
-			a:        kargoapi.PromotionPhasePending,
-			b:        kargoapi.PromotionPhasePending,
-			expected: 0,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			require.Equal(t, tt.expected, comparePromotionPhase(tt.a, tt.b))
-		})
-	}
-}
-
 func fakeNow() time.Time {
 	return fakeTime
 }
diff --git a/ui/src/features/stage/promotions.test.ts b/ui/src/features/stage/promotions.test.ts
deleted file mode 100644
index e0c9693e..00000000
--- a/ui/src/features/stage/promotions.test.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Timestamp } from '@bufbuild/protobuf';
-import { describe, expect, test } from 'vitest';
-
-import { ObjectMeta } from '@ui/gen/k8s.io/apimachinery/pkg/apis/meta/v1/generated_pb';
-import { Promotion, PromotionStatus } from '@ui/gen/v1alpha1/generated_pb';
-
-import { sortPromotions } from './utils/sort';
-
-const newTestPromotion = (phase: string, seconds: number): Promotion => {
-  return {
-    metadata: {
-      creationTimestamp: new Timestamp({
-        seconds: BigInt(seconds)
-      })
-    } as ObjectMeta,
-    status: {
-      phase
-    } as PromotionStatus
-  } as Promotion;
-};
-
-const running1 = newTestPromotion('Running', 1);
-const pending2 = newTestPromotion('Pending', 2);
-const running3 = newTestPromotion('Running', 3);
-const pending4 = newTestPromotion('Pending', 4);
-const succeeded5 = newTestPromotion('Succeeded', 5);
-const failed6 = newTestPromotion('Failed', 6);
-const unknown7 = newTestPromotion('Unknown', 7);
-const running8 = newTestPromotion('Running', 8);
-const pending9 = newTestPromotion('Pending', 9);
-const running10 = newTestPromotion('Running', 10);
-const pending11 = newTestPromotion('Pending', 11);
-const succeeded12 = newTestPromotion('Succeeded', 12);
-const errored13 = newTestPromotion('Errored', 13);
-const unknown14 = newTestPromotion('Unknown', 14);
-
-const testPromotions = [
-  running1,
-  pending2,
-  running3,
-  pending4,
-  succeeded5,
-  failed6,
-  unknown7,
-  running8,
-  pending9,
-  running10,
-  pending11,
-  succeeded12,
-  errored13,
-  unknown14
-];
-
-const orderedPromotions = [
-  running10,
-  running8,
-  running3,
-  running1,
-  pending11,
-  pending9,
-  pending4,
-  pending2,
-  unknown14,
-  errored13,
-  succeeded12,
-  unknown7,
-  failed6,
-  succeeded5
-];
-
-describe('sortPromotions', () => {
-  test('sorts promotions', () => {
-    expect(testPromotions.sort(sortPromotions)).toEqual(orderedPromotions);
-  });
-});