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,