From f6b2bedd69faaaf1308d6fcc13253cbf503f3d73 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Fri, 22 May 2026 13:28:32 +0200 Subject: [PATCH] feat(agent/v2): realmjoin_phase_changed surfaces RJ enum names alongside raw DeploymentPhase ints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The realmjoin_phase_changed message read like "DeploymentPhase 100 -> 110" — the raw ints are correct but require a lookup table to interpret. Map them to the RJ-internal DeploymentPhase enum names (Blank / RunningFirstDeployment[Auto] / CompletedFirstDeployment / RunningDeployment / CompletedDeployment / ManualDeployment / NestedFromShim) so the timeline + log message read like "DeploymentPhase RunningFirstDeployment -> CompletedFirstDeployment" without a lookup. The numeric ints stay in the payload (deploymentPhase / previousPhase) so KQL filters and dashboards do not have to switch. - New RealmJoinInfo.PhaseDisplayName(int) — single switch matching the published RJ enum. Unknown / future values fall back to the numeric string so they stay observable instead of being collapsed to "Unknown". - RealmJoinWatcherAdapter.OnPhaseChanged: payload now carries the two new deploymentPhaseName / previousPhaseName keys; message rendered with names. Tests: +1 (mapping known phase ints → enum names + message format), existing phase-changed test extended to assert both raw + named fields. 11/11 adapter suite green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../RealmJoinWatcherAdapterTests.cs | 24 +++++++++++++++++++ .../Enrollment/SystemSignals/RealmJoinInfo.cs | 22 +++++++++++++++++ .../SignalAdapters/RealmJoinWatcherAdapter.cs | 9 ++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs index e7b5ebb6..4ad27ab0 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs @@ -140,8 +140,32 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters && p.Payload != null && p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et) && et == SharedEventTypes.RealmJoinPhaseChanged); + // Raw ints stay for downstream filters / KQL. Assert.Equal("200", info.Payload!["deploymentPhase"]); Assert.Equal("100", info.Payload["previousPhase"]); + // RJ enum names alongside — UI / message readable without a lookup table. + Assert.Equal("RunningDeployment", info.Payload["deploymentPhaseName"]); + Assert.Equal("RunningFirstDeployment", info.Payload["previousPhaseName"]); + Assert.Contains("RunningFirstDeployment -> RunningDeployment", info.Payload[SignalPayloadKeys.Message]); + } + + [Fact] + public void TriggerPhaseChangedFromTest_falls_back_to_numeric_when_phase_is_unknown_RJ_enum_value() + { + using var f = new Fixture(); + using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock); + + adapter.TriggerPhaseChangedFromTest(prev: 100, curr: 999); + + var info = f.Ingress.Posted.Single(p => + p.Kind == DecisionSignalKind.InformationalEvent + && p.Payload != null + && p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et) + && et == SharedEventTypes.RealmJoinPhaseChanged); + Assert.Equal("RunningFirstDeployment", info.Payload!["previousPhaseName"]); + // Future / unknown RJ phase values stay observable as their numeric form rather + // than being collapsed to "Unknown". + Assert.Equal("999", info.Payload["deploymentPhaseName"]); } [Fact] diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs index a261465d..d133b5df 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs @@ -66,6 +66,28 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals // Resolved-phase value per the RJ enum (RealmJoin.Core.SoftwarePackaging.DeploymentPhase). public const int PhaseCompletedFirstDeployment = 110; + /// + /// Map a raw DeploymentPhase DWORD to its RJ-internal enum name. Mirrors + /// RealmJoin.Core.SoftwarePackaging.DeploymentPhase as documented at feature + /// design time. Unknown values fall back to the numeric form so future RJ-introduced + /// phases stay observable instead of being collapsed to "Unknown". + /// + public static string PhaseDisplayName(int phase) + { + switch (phase) + { + case 0: return "Blank"; + case 100: return "RunningFirstDeployment"; + case 101: return "RunningFirstDeploymentAuto"; + case 110: return "CompletedFirstDeployment"; + case 200: return "RunningDeployment"; + case 210: return "CompletedDeployment"; + case 500: return "ManualDeployment"; + case 600: return "NestedFromShim"; + default: return phase.ToString(CultureInfo.InvariantCulture); + } + } + /// Canonical 64-bit install path for the RJ host executable. public const string RealmJoinExePath = @"C:\Program Files\RealmJoin\RealmJoin.exe"; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/RealmJoinWatcherAdapter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/RealmJoinWatcherAdapter.cs index d656e5d1..1a01e377 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/RealmJoinWatcherAdapter.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/RealmJoinWatcherAdapter.cs @@ -129,15 +129,22 @@ namespace AutopilotMonitor.Agent.V2.Core.SignalAdapters private void OnPhaseChanged(object? sender, RealmJoinPhaseChangedEventArgs e) { // Phase change is observability-only: no DecisionSignalKind, just a timeline event. + // Both raw int (deploymentPhase / previousPhase) and human-readable name + // (deploymentPhaseName / previousPhaseName) flow through — int stays for downstream + // filters / KQL, name fields make the message + UI rows readable without a lookup. + var currentName = RealmJoinInfo.PhaseDisplayName(e.CurrentPhase); + var previousName = RealmJoinInfo.PhaseDisplayName(e.PreviousPhase); var data = new Dictionary(StringComparer.Ordinal) { ["deploymentPhase"] = e.CurrentPhase.ToString(), + ["deploymentPhaseName"] = currentName, ["previousPhase"] = e.PreviousPhase.ToString(), + ["previousPhaseName"] = previousName, }; _post.Emit( eventType: SharedConstants.EventTypes.RealmJoinPhaseChanged, source: SourceLabel, - message: $"RealmJoin DeploymentPhase {e.PreviousPhase} -> {e.CurrentPhase}", + message: $"RealmJoin DeploymentPhase {previousName} -> {currentName}", severity: EventSeverity.Info, immediateUpload: false, data: data,