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 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 22:25:04 +02:00
parent e4ccce1e3b
commit 24464c0772
2 changed files with 65 additions and 4 deletions

View File

@@ -51,7 +51,7 @@ public class DirtyStateCalculator {
if (!(desired instanceof ObjectNode desiredObj) || !(deployed instanceof ObjectNode deployedObj)) { if (!(desired instanceof ObjectNode desiredObj) || !(deployed instanceof ObjectNode deployedObj)) {
if (!Objects.equals(desired, deployed)) { if (!Objects.equals(desired, deployed)) {
diffs.add(new DirtyStateResult.Difference(prefix, diffs.add(new DirtyStateResult.Difference(prefix,
String.valueOf(desired), String.valueOf(deployed))); nodeToString(desired), nodeToString(deployed)));
} }
return; return;
} }
@@ -61,10 +61,18 @@ public class DirtyStateCalculator {
for (String key : keys) { for (String key : keys) {
JsonNode d = desiredObj.get(key); JsonNode d = desiredObj.get(key);
JsonNode p = deployedObj.get(key); JsonNode p = deployedObj.get(key);
if (!Objects.equals(d, p)) { if (Objects.equals(d, p)) continue;
diffs.add(new DirtyStateResult.Difference(prefix + "." + key, if (d instanceof ObjectNode && p instanceof ObjectNode) {
String.valueOf(d), String.valueOf(p))); 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
}
} }

View File

@@ -88,4 +88,57 @@ class DirtyStateCalculatorTest {
assertThat(result.differences()).extracting(DirtyStateResult.Difference::field) assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
.contains("containerConfig.memoryLimitMb"); .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");
}
} }