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,