From c688ac64f4e6b0639f964b6742cd0708200310f7 Mon Sep 17 00:00:00 2001 From: Harris Effron Date: Tue, 14 Jul 2026 15:17:40 -0400 Subject: [PATCH] Fix SortAlphabetically's remote-kill comparator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two-key comparison (Split < Split && Reason < Reason) is not a strict weak ordering, so remote_kills order in the written schema depended on input order — a source of spurious diffs on every schema write. Co-Authored-By: Claude Fable 5 --- schema/schema.go | 6 ++++-- schema/schema_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 schema/schema_test.go diff --git a/schema/schema.go b/schema/schema.go index 3992a0d..e32bebd 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -210,8 +210,10 @@ func applyAllMigrationsToSchema(schema *serializers.Schema) error { // SortAlphabetically sorts the schema's resource slices by their natural keys func SortAlphabetically(schema *serializers.Schema) { sort.Slice(schema.RemoteKills, func(i, j int) bool { - return schema.RemoteKills[i].Split < schema.RemoteKills[j].Split && - schema.RemoteKills[i].Reason < schema.RemoteKills[j].Reason + if schema.RemoteKills[i].Split != schema.RemoteKills[j].Split { + return schema.RemoteKills[i].Split < schema.RemoteKills[j].Split + } + return schema.RemoteKills[i].Reason < schema.RemoteKills[j].Reason }) sort.Slice(schema.FeatureCompletions, func(i, j int) bool { return schema.FeatureCompletions[i].FeatureGate < schema.FeatureCompletions[j].FeatureGate diff --git a/schema/schema_test.go b/schema/schema_test.go new file mode 100644 index 0000000..65a2c65 --- /dev/null +++ b/schema/schema_test.go @@ -0,0 +1,28 @@ +package schema + +import ( + "testing" + + "github.com/Betterment/testtrack-cli/serializers" + "github.com/stretchr/testify/require" +) + +func TestSortAlphabeticallyRemoteKillsIsDeterministic(t *testing.T) { + // These three kills are mutually incomparable under the old buggy + // comparator (Split < Split && Reason < Reason), which made the written + // order depend on input order. + kills := []serializers.RemoteKill{ + {Split: "a_split", Reason: "z_reason"}, + {Split: "b_split", Reason: "m_reason"}, + {Split: "c_split", Reason: "a_reason"}, + } + forward := &serializers.Schema{RemoteKills: []serializers.RemoteKill{kills[0], kills[1], kills[2]}} + reversed := &serializers.Schema{RemoteKills: []serializers.RemoteKill{kills[2], kills[1], kills[0]}} + + SortAlphabetically(forward) + SortAlphabetically(reversed) + + require.Equal(t, forward.RemoteKills, reversed.RemoteKills, + "remote kills must sort to the same order regardless of input order") + require.Equal(t, []serializers.RemoteKill{kills[0], kills[1], kills[2]}, forward.RemoteKills) +}