From d73a063ab2258d72ea75cfa8820a7d59073be16a Mon Sep 17 00:00:00 2001 From: Dan Marshall <16818261+demolitionmode@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:13:16 +0100 Subject: [PATCH 1/4] test(diff): add list of string to structured diff test manifest --- diff/diff_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/diff/diff_test.go b/diff/diff_test.go index b3965e80..a9292111 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -602,6 +602,15 @@ spec: containers: - name: app image: demo:v1 + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-type + operator: In + values: + - standard ` newManifest := ` apiVersion: apps/v1 @@ -616,6 +625,15 @@ spec: containers: - name: app image: demo:v2 + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-type + operator: In + values: + - dedicated ` oldIndex := manifest.Parse([]byte(oldManifest), "prod", true) newIndex := manifest.Parse([]byte(newManifest), "prod", true) From 59e16ca4809dca96e79b0b1cddf2def600e33ffe Mon Sep 17 00:00:00 2001 From: Dan Marshall <16818261+demolitionmode@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:56:03 +0100 Subject: [PATCH 2/4] fix(diff): handle array of strings in structured diff generation --- diff/diff_test.go | 7 ++++++- diff/structured.go | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/diff/diff_test.go b/diff/diff_test.go index a9292111..24d5422c 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -651,7 +651,7 @@ spec: require.Equal(t, "Deployment", entry.Kind) require.Equal(t, "prod", entry.Namespace) require.Equal(t, "web", entry.Name) - require.Len(t, entry.Changes, 2) + require.Len(t, entry.Changes, 3) replicasChange, ok := findChange(entry.Changes, "spec", "replicas") require.True(t, ok) require.InDelta(t, float64(2), replicasChange.OldValue, 0.001) @@ -661,6 +661,11 @@ spec: require.True(t, ok) require.Equal(t, "demo:v1", imageChange.OldValue) require.Equal(t, "demo:v2", imageChange.NewValue) + + affinityChange, ok := findChange(entry.Changes, "spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values", "0") + require.True(t, ok) + require.Equal(t, "standard", affinityChange.OldValue) + require.Equal(t, "dedicated", affinityChange.NewValue) } func TestStructuredOutputAddAndRemove(t *testing.T) { diff --git a/diff/structured.go b/diff/structured.go index 6b0f1238..6d893269 100644 --- a/diff/structured.go +++ b/diff/structured.go @@ -271,6 +271,20 @@ func diffArrayNodes(changes *[]FieldChange, tokens []string, oldNode, newNode in if reflect.DeepEqual(oldVal, newVal) { continue } + // If both oldVal and newVal are strings, we can directly record the change without further recursion. + if oldStr, ok := oldVal.(string); ok { + if newStr, ok := newVal.(string); ok { + path, field := splitTokens(next) + *changes = append(*changes, FieldChange{ + Path: path, + Field: field, + Change: "replace", + OldValue: oldStr, + NewValue: newStr, + }) + continue + } + } subPatch, err := createNodePatch(oldVal, newVal) if err != nil { return err From 96681bde42451002faa14ee71c0970e7ba612f7c Mon Sep 17 00:00:00 2001 From: Dan Marshall <16818261+demolitionmode@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:15:59 +0100 Subject: [PATCH 3/4] fix(diff): handle array of scalars in structured diff generation --- diff/diff_test.go | 36 ++++++++++++++++++++++++++++++++++-- diff/structured.go | 36 +++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/diff/diff_test.go b/diff/diff_test.go index 24d5422c..096ab352 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -611,6 +611,10 @@ spec: operator: In values: - standard + priorities: + - 1 + enabled: + - true ` newManifest := ` apiVersion: apps/v1 @@ -634,6 +638,10 @@ spec: operator: In values: - dedicated + priorities: + - 2 + enabled: + - false ` oldIndex := manifest.Parse([]byte(oldManifest), "prod", true) newIndex := manifest.Parse([]byte(newManifest), "prod", true) @@ -651,7 +659,7 @@ spec: require.Equal(t, "Deployment", entry.Kind) require.Equal(t, "prod", entry.Namespace) require.Equal(t, "web", entry.Name) - require.Len(t, entry.Changes, 3) + require.Len(t, entry.Changes, 5) replicasChange, ok := findChange(entry.Changes, "spec", "replicas") require.True(t, ok) require.InDelta(t, float64(2), replicasChange.OldValue, 0.001) @@ -666,6 +674,16 @@ spec: require.True(t, ok) require.Equal(t, "standard", affinityChange.OldValue) require.Equal(t, "dedicated", affinityChange.NewValue) + + priorityChange, ok := findChange(entry.Changes, "spec.template.spec.priorities", "0") + require.True(t, ok) + require.InDelta(t, float64(1), priorityChange.OldValue, 0.001) + require.InDelta(t, float64(2), priorityChange.NewValue, 0.001) + + enabledChange, ok := findChange(entry.Changes, "spec.template.spec.enabled", "0") + require.True(t, ok) + require.Equal(t, true, enabledChange.OldValue) + require.Equal(t, false, enabledChange.NewValue) } func TestStructuredOutputAddAndRemove(t *testing.T) { @@ -677,7 +695,20 @@ kind: Job metadata: name: migrate namespace: ops -spec: {} +spec: + restartPolicy: "Never" + containers: + - name: app + image: demo:v1 + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-type + operator: In + values: + - standard ` newIndex := manifest.Parse([]byte(newManifest), "ops", true) @@ -688,6 +719,7 @@ spec: {} var entries []StructuredEntry require.NoError(t, json.Unmarshal(buf.Bytes(), &entries)) require.Len(t, entries, 1) + require.Equal(t, "ADD", entries[0].ChangeType) require.True(t, entries[0].ResourceStatus.NewExists) require.False(t, entries[0].ResourceStatus.OldExists) diff --git a/diff/structured.go b/diff/structured.go index 6d893269..55d7f9f1 100644 --- a/diff/structured.go +++ b/diff/structured.go @@ -271,19 +271,17 @@ func diffArrayNodes(changes *[]FieldChange, tokens []string, oldNode, newNode in if reflect.DeepEqual(oldVal, newVal) { continue } - // If both oldVal and newVal are strings, we can directly record the change without further recursion. - if oldStr, ok := oldVal.(string); ok { - if newStr, ok := newVal.(string); ok { - path, field := splitTokens(next) - *changes = append(*changes, FieldChange{ - Path: path, - Field: field, - Change: "replace", - OldValue: oldStr, - NewValue: newStr, - }) - continue - } + // createNodePatch only supports JSON objects; handle scalar values directly + if isScalarValue(oldVal) && isScalarValue(newVal) { + path, field := splitTokens(next) + *changes = append(*changes, FieldChange{ + Path: path, + Field: field, + Change: "replace", + OldValue: oldVal, + NewValue: newVal, + }) + continue } subPatch, err := createNodePatch(oldVal, newVal) if err != nil { @@ -325,6 +323,18 @@ func diffArrayNodes(changes *[]FieldChange, tokens []string, oldNode, newNode in return nil } +func isScalarValue(value interface{}) bool { + switch value.(type) { + case string, bool, + float32, float64, + int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64: + return true + default: + return false + } +} + func createNodePatch(oldNode, newNode interface{}) (interface{}, error) { oldJSON, err := json.Marshal(oldNode) if err != nil { From 3be1645ecb5549b64487ca93d4ca1457add5b269 Mon Sep 17 00:00:00 2001 From: Dan Marshall <16818261+demolitionmode@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:45:54 +0100 Subject: [PATCH 4/4] test(diff): add multi-type array test --- diff/diff_test.go | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/diff/diff_test.go b/diff/diff_test.go index 096ab352..0a9e3ea6 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -613,8 +613,12 @@ spec: - standard priorities: - 1 - enabled: + booleanArrayField: - true + multiTypeField: + - 3 + - true + - "string" ` newManifest := ` apiVersion: apps/v1 @@ -640,8 +644,12 @@ spec: - dedicated priorities: - 2 - enabled: + booleanArrayField: + - false + multiTypeField: - false + - "new-string" + - 2 ` oldIndex := manifest.Parse([]byte(oldManifest), "prod", true) newIndex := manifest.Parse([]byte(newManifest), "prod", true) @@ -659,7 +667,7 @@ spec: require.Equal(t, "Deployment", entry.Kind) require.Equal(t, "prod", entry.Namespace) require.Equal(t, "web", entry.Name) - require.Len(t, entry.Changes, 5) + require.Len(t, entry.Changes, 8) replicasChange, ok := findChange(entry.Changes, "spec", "replicas") require.True(t, ok) require.InDelta(t, float64(2), replicasChange.OldValue, 0.001) @@ -680,10 +688,25 @@ spec: require.InDelta(t, float64(1), priorityChange.OldValue, 0.001) require.InDelta(t, float64(2), priorityChange.NewValue, 0.001) - enabledChange, ok := findChange(entry.Changes, "spec.template.spec.enabled", "0") + booleanArrayFieldChange, ok := findChange(entry.Changes, "spec.template.spec.booleanArrayField", "0") + require.True(t, ok) + require.Equal(t, true, booleanArrayFieldChange.OldValue) + require.Equal(t, false, booleanArrayFieldChange.NewValue) + + multiTypeFieldChange0, ok := findChange(entry.Changes, "spec.template.spec.multiTypeField", "0") + require.True(t, ok) + require.InDelta(t, float64(3), multiTypeFieldChange0.OldValue, 0.001) + require.Equal(t, false, multiTypeFieldChange0.NewValue) + + multiTypeFieldChange1, ok := findChange(entry.Changes, "spec.template.spec.multiTypeField", "1") + require.True(t, ok) + require.Equal(t, true, multiTypeFieldChange1.OldValue) + require.Equal(t, "new-string", multiTypeFieldChange1.NewValue) + + multiTypeFieldChange2, ok := findChange(entry.Changes, "spec.template.spec.multiTypeField", "2") require.True(t, ok) - require.Equal(t, true, enabledChange.OldValue) - require.Equal(t, false, enabledChange.NewValue) + require.Equal(t, "string", multiTypeFieldChange2.OldValue) + require.InDelta(t, float64(2), multiTypeFieldChange2.NewValue, 0.001) } func TestStructuredOutputAddAndRemove(t *testing.T) {