From 24464c077213cc44ca61cc08ae06e8b4de3a0df9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:25:04 +0200 Subject: [PATCH] core(deploy): recurse into nested diffs + unquote scalar values in DirtyStateCalculator - compareJson now recurses when both nodes are ObjectNode, so nested maps (tracedProcessors, routeRecording, routeSamplingRates) produce deep paths like agentConfig.tracedProcessors.proc-1 instead of a blob diff - Extract nodeToString helper: value nodes use asText() (strips JSON quotes), null becomes "(none)", arrays/objects get compact JSON - Apply nodeToString in both diff-emission paths (top-level mismatch + leaf) - Add three new tests: nullAgentConfigInSnapshot, nestedAgentField_reportsDeepPath, stringField_differenceValueIsUnquoted (8 tests total, all pass) Co-Authored-By: Claude Sonnet 4.6 --- .../core/runtime/DirtyStateCalculator.java | 16 ++++-- .../runtime/DirtyStateCalculatorTest.java | 53 +++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DirtyStateCalculator.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DirtyStateCalculator.java index f9a32ad6..2fc24b53 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DirtyStateCalculator.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DirtyStateCalculator.java @@ -51,7 +51,7 @@ public class DirtyStateCalculator { if (!(desired instanceof ObjectNode desiredObj) || !(deployed instanceof ObjectNode deployedObj)) { if (!Objects.equals(desired, deployed)) { diffs.add(new DirtyStateResult.Difference(prefix, - String.valueOf(desired), String.valueOf(deployed))); + nodeToString(desired), nodeToString(deployed))); } return; } @@ -61,10 +61,18 @@ public class DirtyStateCalculator { for (String key : keys) { JsonNode d = desiredObj.get(key); JsonNode p = deployedObj.get(key); - if (!Objects.equals(d, p)) { - diffs.add(new DirtyStateResult.Difference(prefix + "." + key, - String.valueOf(d), String.valueOf(p))); + if (Objects.equals(d, p)) continue; + if (d instanceof ObjectNode && p instanceof ObjectNode) { + compareJson(prefix + "." + key, d, p, diffs); + } else { + diffs.add(new DirtyStateResult.Difference(prefix + "." + key, nodeToString(d), nodeToString(p))); } } } + + private static String nodeToString(JsonNode n) { + if (n == null) return "(none)"; + if (n.isValueNode()) return n.asText(); + return n.toString(); // arrays/objects: compact JSON + } } diff --git a/cameleer-server-core/src/test/java/com/cameleer/server/core/runtime/DirtyStateCalculatorTest.java b/cameleer-server-core/src/test/java/com/cameleer/server/core/runtime/DirtyStateCalculatorTest.java index ba9ec733..bb57be38 100644 --- a/cameleer-server-core/src/test/java/com/cameleer/server/core/runtime/DirtyStateCalculatorTest.java +++ b/cameleer-server-core/src/test/java/com/cameleer/server/core/runtime/DirtyStateCalculatorTest.java @@ -88,4 +88,57 @@ class DirtyStateCalculatorTest { assertThat(result.differences()).extracting(DirtyStateResult.Difference::field) .contains("containerConfig.memoryLimitMb"); } + + @Test + void nullAgentConfigInSnapshot_marksAgentConfigDiff() { + DirtyStateCalculator calc = new DirtyStateCalculator(); + ApplicationConfig desired = new ApplicationConfig(); + desired.setSamplingRate(1.0); + UUID jarId = UUID.randomUUID(); + DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, null, Map.of()); + + DirtyStateResult result = calc.compute(jarId, desired, Map.of(), snap); + + assertThat(result.dirty()).isTrue(); + assertThat(result.differences()).extracting(DirtyStateResult.Difference::field) + .contains("agentConfig"); + } + + @Test + void nestedAgentField_reportsDeepPath() { + DirtyStateCalculator calc = new DirtyStateCalculator(); + + ApplicationConfig deployed = new ApplicationConfig(); + deployed.setTracedProcessors(Map.of("proc-1", "DEBUG")); + ApplicationConfig desired = new ApplicationConfig(); + desired.setTracedProcessors(Map.of("proc-1", "TRACE")); + UUID jarId = UUID.randomUUID(); + DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployed, Map.of()); + + DirtyStateResult result = calc.compute(jarId, desired, Map.of(), snap); + + assertThat(result.dirty()).isTrue(); + assertThat(result.differences()).extracting(DirtyStateResult.Difference::field) + .contains("agentConfig.tracedProcessors.proc-1"); + } + + @Test + void stringField_differenceValueIsUnquoted() { + DirtyStateCalculator calc = new DirtyStateCalculator(); + + ApplicationConfig deployed = new ApplicationConfig(); + deployed.setApplicationLogLevel("INFO"); + ApplicationConfig desired = new ApplicationConfig(); + desired.setApplicationLogLevel("DEBUG"); + UUID jarId = UUID.randomUUID(); + DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployed, Map.of()); + + DirtyStateResult result = calc.compute(jarId, desired, Map.of(), snap); + + DirtyStateResult.Difference diff = result.differences().stream() + .filter(d -> d.field().equals("agentConfig.applicationLogLevel")) + .findFirst().orElseThrow(); + assertThat(diff.staged()).isEqualTo("DEBUG"); + assertThat(diff.deployed()).isEqualTo("INFO"); + } }