From c5b6f2bbadd54ca70920fec2557fbd9c0d69f0f4 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:42:07 +0200 Subject: [PATCH] fix(dirty-state): exclude live-pushed fields from deploy diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-pushed config fields (taps, tapVersion, tracedProcessors, routeRecording) apply via SSE CONFIG_UPDATE — they take effect on running agents without a redeploy and are fetched on agent restart from application_config. They must not contribute to the "pending deploy" diff against the last-successful-deployment snapshot. Before this fix, applying a tap from the process diagram correctly rolled out in real time but then marked the app "Pending Deploy (1)" because DirtyStateCalculator compared every agentConfig field. This also contradicted the UI rule (ui.md) that the live tabs "never mark dirty". Adds taps, tapVersion, tracedProcessors, routeRecording to AGENT_CONFIG_IGNORED_KEYS. Updates the nested-path test to use a staged field (sensitiveKeys) and adds a new test asserting that divergent live-push fields keep dirty=false. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../core/runtime/DirtyStateCalculator.java | 7 ++++- .../runtime/DirtyStateCalculatorTest.java | 29 +++++++++++++++++-- 2 files changed, 32 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 5239922f..9010bf5a 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 @@ -23,8 +23,13 @@ import java.util.UUID; */ public class DirtyStateCalculator { + // Live-pushed fields are excluded from the deploy diff: changes to them take effect + // via SSE config-update without a redeploy, so they are not "pending deploy" when they + // differ from the last successful deployment snapshot. See ui/rules: the Traces & Taps + // and Route Recording tabs apply with ?apply=live and "never mark dirty". private static final Set AGENT_CONFIG_IGNORED_KEYS = Set.of( - "version", "updatedAt", "updatedBy", "environment", "application" + "version", "updatedAt", "updatedBy", "environment", "application", + "taps", "tapVersion", "tracedProcessors", "routeRecording" ); private final ObjectMapper mapper; 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 600e5a40..fa035724 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 @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -114,9 +115,9 @@ class DirtyStateCalculatorTest { DirtyStateCalculator calc = CALC; ApplicationConfig deployed = new ApplicationConfig(); - deployed.setTracedProcessors(Map.of("proc-1", "DEBUG")); + deployed.setSensitiveKeys(List.of("password", "token")); ApplicationConfig desired = new ApplicationConfig(); - desired.setTracedProcessors(Map.of("proc-1", "TRACE")); + desired.setSensitiveKeys(List.of("password", "token", "secret")); UUID jarId = UUID.randomUUID(); DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployed, Map.of(), null); @@ -124,7 +125,29 @@ class DirtyStateCalculatorTest { assertThat(result.dirty()).isTrue(); assertThat(result.differences()).extracting(DirtyStateResult.Difference::field) - .contains("agentConfig.tracedProcessors.proc-1"); + .anyMatch(f -> f.startsWith("agentConfig.sensitiveKeys")); + } + + @Test + void livePushedFields_doNotMarkDirty() { + // Taps, tracedProcessors, and routeRecording apply via live SSE push (never redeploy), + // so they must not appear as "pending deploy" when they differ from the last deploy snapshot. + ApplicationConfig deployed = new ApplicationConfig(); + deployed.setTracedProcessors(Map.of("proc-1", "DEBUG")); + deployed.setRouteRecording(Map.of("route-a", true)); + deployed.setTapVersion(1); + + ApplicationConfig desired = new ApplicationConfig(); + desired.setTracedProcessors(Map.of("proc-1", "TRACE", "proc-2", "DEBUG")); + desired.setRouteRecording(Map.of("route-a", false, "route-b", true)); + desired.setTapVersion(5); + + UUID jarId = UUID.randomUUID(); + DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployed, Map.of(), null); + DirtyStateResult result = CALC.compute(jarId, desired, Map.of(), snap); + + assertThat(result.dirty()).isFalse(); + assertThat(result.differences()).isEmpty(); } @Test