test(decisioncore,agent/v2): RealmJoin gate, adapter, SID resolver tests + signal-kind fixtures (Phase D)

- DecisionCore.Tests/RealmJoinGateTests.cs (9 tests): baseline regression guard,
  RealmJoinDetected arms 60-min timeout, gate defers Finalizing on Classic AND-gate,
  RealmJoinResolved + Timeout both release through CompleteIfDeferredOrBookkeep,
  SelfDeploying terminal defers + direct-complete via SelfDeployingDeferredCompletion,
  per-package facts mutation, and audit-trail attaches realmjoin* fields.
- V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs (6 tests): each of the
  5 watcher events maps to the correct DecisionSignalKind + dual InformationalEvent;
  phase-changed is timeline-only; DisplayName clamped to 256 chars.
- V2.Core.Tests/Monitoring/Runtime/UserSidResolverTests.cs (4 tests): well-known
  BUILTIN\Administrators + NT AUTHORITY\SYSTEM SID round-trips, invalid/empty inputs
  return false.
- tests/fixtures/signal-kinds/realmjoin-{detected,resolved,timeout,package_started,
  package_completed}-v1.json: replay fixtures fulfilling the merge-block contract
  declared on DecisionSignalKind.cs:8.

Verified: 9/9 reducer + 10/10 adapter/SID tests green; no regressions in adjacent
suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-05-21 18:37:36 +02:00
parent 4b247f11a0
commit bc02dad6bc
8 changed files with 555 additions and 0 deletions
@@ -0,0 +1,41 @@
using AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime;
using Xunit;
namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Runtime
{
public sealed class UserSidResolverTests
{
[Fact]
public void Resolves_well_known_builtin_administrators_to_canonical_sid()
{
// BUILTIN\Administrators is a well-known local group with a deterministic SID on
// every Windows installation. Using it as the round-trip oracle avoids any
// environment-specific user-database dependency.
Assert.True(UserSidResolver.TryResolveSid("BUILTIN\\Administrators", out var sid));
Assert.Equal("S-1-5-32-544", sid);
}
[Fact]
public void Resolves_bare_user_form_for_well_known_account()
{
// SYSTEM ("NT AUTHORITY\SYSTEM") is well-known with SID S-1-5-18. The resolver
// must accept the qualified form on every Windows installation.
Assert.True(UserSidResolver.TryResolveSid("NT AUTHORITY\\SYSTEM", out var sid));
Assert.Equal("S-1-5-18", sid);
}
[Fact]
public void Returns_false_for_obviously_invalid_account()
{
Assert.False(UserSidResolver.TryResolveSid("CONTOSO\\definitely-not-a-real-user-7f3e2c9a", out var sid));
Assert.Null(sid);
}
[Fact]
public void Returns_false_for_empty_input()
{
Assert.False(UserSidResolver.TryResolveSid(string.Empty, out var sid));
Assert.Null(sid);
}
}
}
@@ -0,0 +1,190 @@
using System;
using System.Linq;
using AutopilotMonitor.Agent.V2.Core.Logging;
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
using AutopilotMonitor.Agent.V2.Core.SignalAdapters;
using AutopilotMonitor.Agent.V2.Core.Tests.Harness;
using AutopilotMonitor.Agent.V2.Core.Tests.Orchestration;
using AutopilotMonitor.DecisionCore.Engine;
using AutopilotMonitor.DecisionCore.Signals;
using AutopilotMonitor.DecisionCore.State;
using SharedEventTypes = AutopilotMonitor.Shared.Constants.EventTypes;
using Xunit;
namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters
{
public sealed class RealmJoinWatcherAdapterTests
{
private static readonly DateTime Fixed = new DateTime(2026, 5, 21, 10, 0, 0, DateTimeKind.Utc);
private sealed class Fixture : IDisposable
{
public TempDirectory Tmp { get; } = new TempDirectory();
public AgentLogger Logger { get; }
public RealmJoinWatcher Watcher { get; }
public FakeSignalIngressSink Ingress { get; } = new FakeSignalIngressSink();
public VirtualClock Clock { get; } = new VirtualClock(Fixed);
public Fixture()
{
Logger = new AgentLogger(Tmp.Path, AgentLogLevel.Info);
Watcher = new RealmJoinWatcher(Logger);
}
public void Dispose()
{
Watcher.Dispose();
Tmp.Dispose();
}
}
[Fact]
public void TriggerDetectedFromTest_emits_RealmJoinDetected_signal_and_informational_event()
{
using var f = new Fixture();
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
adapter.TriggerDetectedFromTest(phase: 100);
// 1) Decision signal that mutates engine state.
var decision = f.Ingress.Posted.Single(p => p.Kind == DecisionSignalKind.RealmJoinDetected);
Assert.Equal("RealmJoinWatcher", decision.SourceOrigin);
Assert.Equal("100", decision.Payload![DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase]);
// 2) Dual-emit informational event so the timeline shows realmjoin_detected.
var info = f.Ingress.Posted.Single(p =>
p.Kind == DecisionSignalKind.InformationalEvent
&& p.Payload != null
&& p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et)
&& et == SharedEventTypes.RealmJoinDetected);
Assert.Equal("RealmJoinWatcher", info.Payload![SignalPayloadKeys.Source]);
Assert.Equal("true", info.Payload[SignalPayloadKeys.ImmediateUpload]);
Assert.Equal("100", info.Payload["deploymentPhase"]);
Assert.Contains("HKLM", info.Payload["registryKey"]);
}
[Fact]
public void TriggerResolvedFromTest_emits_RealmJoinResolved_signal_and_informational_event()
{
using var f = new Fixture();
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
adapter.TriggerResolvedFromTest(phase: 110);
var decision = f.Ingress.Posted.Single(p => p.Kind == DecisionSignalKind.RealmJoinResolved);
Assert.Equal("110", decision.Payload![DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase]);
var info = f.Ingress.Posted.Single(p =>
p.Kind == DecisionSignalKind.InformationalEvent
&& p.Payload != null
&& p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et)
&& et == SharedEventTypes.RealmJoinResolved);
Assert.Equal("RealmJoinWatcher", info.Payload![SignalPayloadKeys.Source]);
Assert.Equal("110", info.Payload["deploymentPhase"]);
}
[Fact]
public void TriggerPhaseChangedFromTest_emits_informational_only_no_decision_signal()
{
using var f = new Fixture();
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
adapter.TriggerPhaseChangedFromTest(prev: 100, curr: 200);
// Phase change is observability-only. There is no DecisionSignalKind.RealmJoinPhaseChanged.
Assert.DoesNotContain(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.RealmJoinDetected);
Assert.DoesNotContain(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.RealmJoinResolved);
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("200", info.Payload!["deploymentPhase"]);
Assert.Equal("100", info.Payload["previousPhase"]);
}
[Fact]
public void TriggerPackageStartedFromTest_emits_signal_and_event_with_packageId_scope()
{
using var f = new Fixture();
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
var snap = new RealmJoinPackageSnapshot(
packageId: "generic-vlc",
displayName: "VLC media player",
version: "3.0.21.0",
success: null,
lastExitCode: null);
adapter.TriggerPackageStartedFromTest(scope: RealmJoinPackageFact.ScopeMachine, snap: snap);
var decision = f.Ingress.Posted.Single(p => p.Kind == DecisionSignalKind.RealmJoinPackageStarted);
Assert.Equal("generic-vlc", decision.Payload![DecisionEngine.RealmJoinPayloadKeys.PackageId]);
Assert.Equal("VLC media player", decision.Payload[DecisionEngine.RealmJoinPayloadKeys.DisplayName]);
Assert.Equal("3.0.21.0", decision.Payload[DecisionEngine.RealmJoinPayloadKeys.Version]);
Assert.Equal("machine", decision.Payload[DecisionEngine.RealmJoinPayloadKeys.Scope]);
var info = f.Ingress.Posted.Single(p =>
p.Kind == DecisionSignalKind.InformationalEvent
&& p.Payload != null
&& p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et)
&& et == SharedEventTypes.RealmJoinPackageStarted);
Assert.Equal("generic-vlc", info.Payload!["packageId"]);
Assert.Equal("machine", info.Payload["scope"]);
}
[Fact]
public void TriggerPackageCompletedFromTest_emits_signal_and_event_with_success_and_exit_code()
{
using var f = new Fixture();
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
var snap = new RealmJoinPackageSnapshot(
packageId: "generic-vlc",
displayName: "VLC media player",
version: "3.0.21.0",
success: true,
lastExitCode: 0);
adapter.TriggerPackageCompletedFromTest(scope: RealmJoinPackageFact.ScopeMachine, snap: snap);
var decision = f.Ingress.Posted.Single(p => p.Kind == DecisionSignalKind.RealmJoinPackageCompleted);
Assert.Equal("true", decision.Payload![DecisionEngine.RealmJoinPayloadKeys.Success]);
Assert.Equal("0", decision.Payload[DecisionEngine.RealmJoinPayloadKeys.LastExitCode]);
var info = f.Ingress.Posted.Single(p =>
p.Kind == DecisionSignalKind.InformationalEvent
&& p.Payload != null
&& p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et)
&& et == SharedEventTypes.RealmJoinPackageCompleted);
Assert.Equal("true", info.Payload!["success"]);
Assert.Equal("0", info.Payload["lastExitCode"]);
}
[Fact]
public void Overlong_display_name_is_truncated_to_256_characters()
{
using var f = new Fixture();
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
var longName = new string('x', 1000);
var snap = new RealmJoinPackageSnapshot(
packageId: "generic-bigname",
displayName: longName,
version: null,
success: null,
lastExitCode: null);
adapter.TriggerPackageStartedFromTest(scope: RealmJoinPackageFact.ScopeUser, snap: snap);
var info = f.Ingress.Posted.Single(p =>
p.Kind == DecisionSignalKind.InformationalEvent
&& p.Payload != null
&& p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et)
&& et == SharedEventTypes.RealmJoinPackageStarted);
// Adapter must clamp DisplayName before posting (PII / payload-size guard mirror of
// RealmJoinPackageFact.MaxDisplayNameLength = 256). The packageId stays intact.
Assert.Equal(256, info.Payload!["displayName"].Length);
Assert.Equal("generic-bigname", info.Payload["packageId"]);
Assert.Equal("user", info.Payload["scope"]);
}
}
}
@@ -0,0 +1,284 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AutopilotMonitor.DecisionCore.Engine;
using AutopilotMonitor.DecisionCore.Signals;
using AutopilotMonitor.DecisionCore.State;
using AutopilotMonitor.Shared.Models;
using Xunit;
namespace AutopilotMonitor.DecisionCore.Tests
{
/// <summary>
/// Reducer integration tests for the RealmJoin (RJ) completion-gate. The Classic and
/// SelfDeploying completion paths must defer terminal transitions while RJ is detected
/// and unresolved, and release them once <see cref="DecisionSignalKind.RealmJoinResolved"/>
/// arrives or the <see cref="DeadlineNames.RealmJoinTimeout"/> deadline fires.
/// </summary>
public sealed class RealmJoinGateTests
{
private static readonly DateTime T0 = new DateTime(2026, 5, 21, 10, 0, 0, DateTimeKind.Utc);
private static DecisionSignal MakeSignal(
long ordinal,
DecisionSignalKind kind,
DateTime occurredAtUtc,
IReadOnlyDictionary<string, string>? payload = null)
{
return new DecisionSignal(
sessionSignalOrdinal: ordinal,
sessionTraceOrdinal: ordinal,
kind: kind,
kindSchemaVersion: 1,
occurredAtUtc: occurredAtUtc,
sourceOrigin: "test",
evidence: new Evidence(EvidenceKind.Synthetic, $"t-{kind}-{ordinal}", $"synthetic {kind}"),
payload: payload);
}
private static DecisionState PrimeClassicAwaitingDesktop(DecisionEngine engine)
{
var state = DecisionState.CreateInitial("rj-sess", "rj-tenant", T0);
state = engine.Reduce(state, MakeSignal(0, DecisionSignalKind.SessionStarted, T0)).NewState;
state = engine.Reduce(state, MakeSignal(1, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(1),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "DeviceSetup" })).NewState;
state = engine.Reduce(state, MakeSignal(2, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(2),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "AccountSetup" })).NewState;
state = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.EspExiting, T0.AddMinutes(3))).NewState;
state = engine.Reduce(state, MakeSignal(4, DecisionSignalKind.HelloResolved, T0.AddMinutes(4),
new Dictionary<string, string> { [SignalPayloadKeys.HelloOutcome] = "Success" })).NewState;
Assert.Equal(SessionStage.AwaitingDesktop, state.Stage);
return state;
}
// ============================================================== Classic flow
[Fact]
public void Baseline_without_realmjoin_detected_completes_normally()
{
// Regression guard — on devices where RJ is not installed (DetectedUtc stays null),
// the gate is open and the Hello+Desktop AND-gate must reach Finalizing exactly as
// before Phase A.
var engine = new DecisionEngine();
var state = PrimeClassicAwaitingDesktop(engine);
Assert.Null(state.RealmJoinFacts.DetectedUtc);
var step = engine.Reduce(state, MakeSignal(5, DecisionSignalKind.DesktopArrived, T0.AddMinutes(5)));
Assert.Equal(SessionStage.Finalizing, step.NewState.Stage);
Assert.Contains(step.NewState.Deadlines, d => d.Name == DeadlineNames.FinalizingGrace);
}
[Fact]
public void RealmJoinDetected_arms_the_60_min_hard_timeout_deadline()
{
var engine = new DecisionEngine();
var state = PrimeClassicAwaitingDesktop(engine);
var step = engine.Reduce(state, MakeSignal(5, DecisionSignalKind.RealmJoinDetected, T0.AddMinutes(5),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "100" }));
Assert.NotNull(step.NewState.RealmJoinFacts.DetectedUtc);
Assert.Equal(100, step.NewState.RealmJoinFacts.LastDeploymentPhase!.Value);
var deadline = Assert.Single(step.NewState.Deadlines, d => d.Name == DeadlineNames.RealmJoinTimeout);
// 60-min hard timeout from the post-PrimeClassic AgentBoot anchor (T0) — within rounding.
Assert.InRange(deadline.DueAtUtc, T0.AddMinutes(55), T0.AddMinutes(70));
// ScheduleDeadline effect emitted exactly once.
Assert.Single(step.Effects, e =>
e.Kind == DecisionEffectKind.ScheduleDeadline &&
e.Deadline != null &&
e.Deadline.Name == DeadlineNames.RealmJoinTimeout);
}
[Fact]
public void RealmJoinGate_closed_defers_finalizing_when_DesktopArrived_arrives_with_hello_already_resolved()
{
// Sequence: Hello + RealmJoinDetected before Desktop, then DesktopArrived. The AND-gate
// would normally TransitionToFinalizing — but the closed gate must keep the session
// out of Finalizing until RJ resolves.
var engine = new DecisionEngine();
var state = PrimeClassicAwaitingDesktop(engine);
state = engine.Reduce(state, MakeSignal(5, DecisionSignalKind.RealmJoinDetected, T0.AddMinutes(5),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "100" })).NewState;
var step = engine.Reduce(state, MakeSignal(6, DecisionSignalKind.DesktopArrived, T0.AddMinutes(6)));
Assert.NotEqual(SessionStage.Finalizing, step.NewState.Stage);
Assert.Null(step.NewState.Outcome);
Assert.DoesNotContain(step.NewState.Deadlines, d => d.Name == DeadlineNames.FinalizingGrace);
Assert.NotNull(step.NewState.DesktopArrivedUtc); // fact still recorded
}
[Fact]
public void RealmJoinResolved_after_hello_and_desktop_triggers_finalizing_via_classic_path()
{
// Same setup as the previous test, then RealmJoinResolved (phase 110) lands. The
// resolved handler routes through CompleteIfDeferredOrBookkeep → TransitionToFinalizing.
var engine = new DecisionEngine();
var state = PrimeClassicAwaitingDesktop(engine);
state = engine.Reduce(state, MakeSignal(5, DecisionSignalKind.RealmJoinDetected, T0.AddMinutes(5),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "100" })).NewState;
state = engine.Reduce(state, MakeSignal(6, DecisionSignalKind.DesktopArrived, T0.AddMinutes(6))).NewState;
Assert.NotEqual(SessionStage.Finalizing, state.Stage);
var step = engine.Reduce(state, MakeSignal(7, DecisionSignalKind.RealmJoinResolved, T0.AddMinutes(7),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "110" }));
Assert.Equal(SessionStage.Finalizing, step.NewState.Stage);
Assert.Contains(step.NewState.Deadlines, d => d.Name == DeadlineNames.FinalizingGrace);
// RealmJoinTimeout deadline got cancelled both in state and as a scheduler effect.
Assert.DoesNotContain(step.NewState.Deadlines, d => d.Name == DeadlineNames.RealmJoinTimeout);
Assert.Contains(step.Effects, e =>
e.Kind == DecisionEffectKind.CancelDeadline &&
e.CancelDeadlineName == DeadlineNames.RealmJoinTimeout);
Assert.Equal("Resolved", step.NewState.RealmJoinFacts.Outcome!.Value);
}
[Fact]
public void RealmJoinTimeout_with_hello_and_desktop_in_completes_with_timeout_outcome()
{
// Hard 60-min timeout fires while RJ is still incomplete but Hello+Desktop already
// landed. Handler routes through CompleteIfDeferredOrBookkeep → TransitionToFinalizing
// and records realmjoinOutcome="Timeout".
var engine = new DecisionEngine();
var state = PrimeClassicAwaitingDesktop(engine);
state = engine.Reduce(state, MakeSignal(5, DecisionSignalKind.RealmJoinDetected, T0.AddMinutes(5),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "100" })).NewState;
state = engine.Reduce(state, MakeSignal(6, DecisionSignalKind.DesktopArrived, T0.AddMinutes(6))).NewState;
var step = engine.Reduce(state, MakeSignal(7, DecisionSignalKind.DeadlineFired, T0.AddMinutes(65),
new Dictionary<string, string> { [SignalPayloadKeys.Deadline] = DeadlineNames.RealmJoinTimeout }));
Assert.Equal(SessionStage.Finalizing, step.NewState.Stage);
Assert.Equal("Timeout", step.NewState.RealmJoinFacts.Outcome!.Value);
// The reducer emitted a realmjoin_timeout timeline entry alongside the
// Finalizing-transition effects.
Assert.Contains(step.Effects, e =>
e.Kind == DecisionEffectKind.EmitEventTimelineEntry &&
e.Parameters != null &&
e.Parameters.TryGetValue("eventType", out var et) && et == "realmjoin_timeout");
}
// ============================================================== SelfDeploying flow
[Fact]
public void SelfDeploying_DeviceSetupProvisioningComplete_with_realmjoin_open_completes_immediately()
{
// Baseline — RJ never detected → SelfDeploying terminal path completes directly.
var engine = new DecisionEngine();
var state = DecisionState.CreateInitial("rj-sd", "rj-tenant", T0);
state = engine.Reduce(state, MakeSignal(0, DecisionSignalKind.SessionStarted, T0)).NewState;
state = engine.Reduce(state, MakeSignal(1, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(1),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "DeviceSetup" })).NewState;
var step = engine.Reduce(state, MakeSignal(2, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(2)));
Assert.Equal(SessionStage.Completed, step.NewState.Stage);
Assert.Equal(SessionOutcome.EnrollmentComplete, step.NewState.Outcome);
}
[Fact]
public void SelfDeploying_with_realmjoin_detected_defers_terminal_until_resolved()
{
// SelfDeploying terminal handler observes the gate is closed → marks
// SelfDeployingDeferredCompletion on RealmJoinFacts, stays non-terminal, lets the
// RealmJoinResolved handler complete directly to Completed (not via Finalizing).
var engine = new DecisionEngine();
var state = DecisionState.CreateInitial("rj-sd-2", "rj-tenant", T0);
state = engine.Reduce(state, MakeSignal(0, DecisionSignalKind.SessionStarted, T0)).NewState;
state = engine.Reduce(state, MakeSignal(1, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(1),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "DeviceSetup" })).NewState;
state = engine.Reduce(state, MakeSignal(2, DecisionSignalKind.RealmJoinDetected, T0.AddMinutes(2),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "100" })).NewState;
var deferred = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(3))).NewState;
Assert.NotEqual(SessionStage.Completed, deferred.Stage);
Assert.True(deferred.RealmJoinFacts.SelfDeployingDeferredCompletion?.Value);
var step = engine.Reduce(deferred, MakeSignal(4, DecisionSignalKind.RealmJoinResolved, T0.AddMinutes(4),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "110" }));
Assert.Equal(SessionStage.Completed, step.NewState.Stage);
Assert.Equal(SessionOutcome.EnrollmentComplete, step.NewState.Outcome);
// Direct Completed path emits enrollment_complete inline; no FinalizingGrace deadline
// needed because the SelfDeploying handler clears deadlines.
Assert.DoesNotContain(step.NewState.Deadlines, d => d.Name == DeadlineNames.FinalizingGrace);
Assert.Contains(step.Effects, e =>
e.Kind == DecisionEffectKind.EmitEventTimelineEntry &&
e.Parameters != null &&
e.Parameters.TryGetValue("eventType", out var et) && et == "enrollment_complete");
}
// ============================================================== Per-package tracking
[Fact]
public void Per_package_started_and_completed_signals_update_RealmJoinFacts_packages()
{
var engine = new DecisionEngine();
var state = DecisionState.CreateInitial("rj-pkg", "rj-tenant", T0);
state = engine.Reduce(state, MakeSignal(0, DecisionSignalKind.SessionStarted, T0)).NewState;
state = engine.Reduce(state, MakeSignal(1, DecisionSignalKind.RealmJoinDetected, T0.AddMinutes(1),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "100" })).NewState;
state = engine.Reduce(state, MakeSignal(2, DecisionSignalKind.RealmJoinPackageStarted, T0.AddMinutes(2),
new Dictionary<string, string>
{
[DecisionEngine.RealmJoinPayloadKeys.PackageId] = "generic-vlc",
[DecisionEngine.RealmJoinPayloadKeys.DisplayName] = "VLC media player",
[DecisionEngine.RealmJoinPayloadKeys.Version] = "3.0.21.0",
[DecisionEngine.RealmJoinPayloadKeys.Scope] = RealmJoinPackageFact.ScopeMachine,
})).NewState;
Assert.Single(state.RealmJoinFacts.Packages, p => p.PackageId == "generic-vlc" && p.CompletedUtc == null);
state = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.RealmJoinPackageCompleted, T0.AddMinutes(3),
new Dictionary<string, string>
{
[DecisionEngine.RealmJoinPayloadKeys.PackageId] = "generic-vlc",
[DecisionEngine.RealmJoinPayloadKeys.DisplayName] = "VLC media player",
[DecisionEngine.RealmJoinPayloadKeys.Scope] = RealmJoinPackageFact.ScopeMachine,
[DecisionEngine.RealmJoinPayloadKeys.Success] = "true",
[DecisionEngine.RealmJoinPayloadKeys.LastExitCode] = "0",
})).NewState;
var completed = Assert.Single(state.RealmJoinFacts.Packages, p => p.PackageId == "generic-vlc");
Assert.True(completed.Success);
Assert.Equal(0, completed.LastExitCode);
Assert.NotNull(completed.CompletedUtc);
}
// ============================================================== Audit trail
[Fact]
public void Audit_trail_attaches_realmjoin_fields_when_enrollment_completes_after_resolved()
{
// After SelfDeploying + RealmJoinResolved, the enrollment_complete effect must carry
// the realmjoin* audit-trail fields built by DecisionAuditTrailBuilder.
var engine = new DecisionEngine();
var state = DecisionState.CreateInitial("rj-audit", "rj-tenant", T0);
state = engine.Reduce(state, MakeSignal(0, DecisionSignalKind.SessionStarted, T0)).NewState;
state = engine.Reduce(state, MakeSignal(1, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(1),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "DeviceSetup" })).NewState;
state = engine.Reduce(state, MakeSignal(2, DecisionSignalKind.RealmJoinDetected, T0.AddMinutes(2),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "100" })).NewState;
state = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(3))).NewState;
var step = engine.Reduce(state, MakeSignal(4, DecisionSignalKind.RealmJoinResolved, T0.AddMinutes(4),
new Dictionary<string, string> { [DecisionEngine.RealmJoinPayloadKeys.DeploymentPhase] = "110" }));
var completeEffect = Assert.Single(step.Effects, e =>
e.Kind == DecisionEffectKind.EmitEventTimelineEntry &&
e.Parameters != null &&
e.Parameters.TryGetValue("eventType", out var et) && et == "enrollment_complete");
Assert.NotNull(completeEffect.TypedPayload);
var data = Assert.IsType<Dictionary<string, object>>(completeEffect.TypedPayload);
Assert.Equal("Resolved", data["realmjoinOutcome"]);
Assert.Equal(110, data["realmjoinLastPhase"]);
Assert.True(data.ContainsKey("realmjoinDetectedUtc"));
Assert.True(data.ContainsKey("realmjoinResolvedUtc"));
}
}
}
@@ -0,0 +1,8 @@
# RealmJoinDetected v1 — RealmJoin deployment tracking.
# Posted by RealmJoinWatcherAdapter when HKLM\SYSTEM\CurrentControlSet\Services\realmjoin\
# Parameters is first observed. Payload carries the current DeploymentPhase DWORD.
# Reducer: HandleRealmJoinDetectedV1 → records RealmJoinFacts.DetectedUtc + LastDeploymentPhase,
# arms the 60-min RealmJoinTimeout deadline. The V2 completion AND-gate now reads
# RealmJoinFacts.DetectedUtc != null and defers Finalizing until RealmJoinResolved /
# RealmJoinTimeout fires.
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"RealmJoinDetected","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-21T10:00:00.0000000Z","SourceOrigin":"RealmJoinWatcher","Evidence":{"Kind":"Derived","Identifier":"realmjoin-detected-v1","Summary":"RealmJoin Parameters key observed (phase=100)","DerivationInputs":{"registryKey":"HKLM\\SYSTEM\\CurrentControlSet\\Services\\realmjoin\\Parameters"}},"Payload":{"deploymentPhase":"100"}}
@@ -0,0 +1,7 @@
# RealmJoinPackageCompleted v1 — terminal per-package event.
# Posted by RealmJoinWatcherAdapter when the matching <id> sub-key gains both Success and
# LastExitCode values. Payload extends the started shape with success ("true"|"false") +
# lastExitCode (decimal string of the DWORD).
# Reducer: HandleRealmJoinPackageCompletedV1 → updates the matching RealmJoinPackageFact
# row with CompletedUtc / Success / LastExitCode. Stage unchanged.
{"SessionSignalOrdinal":12,"SessionTraceOrdinal":12,"Kind":"RealmJoinPackageCompleted","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-21T10:08:00.0000000Z","SourceOrigin":"RealmJoinWatcher","Evidence":{"Kind":"Derived","Identifier":"realmjoin-package-completed:machine:generic-vlc","Summary":"RealmJoin package install completed (scope=machine, id=generic-vlc, success=True, exitCode=0)","DerivationInputs":{"registryKey":"HKLM\\SYSTEM\\CurrentControlSet\\Services\\realmjoin\\Parameters"}},"Payload":{"packageId":"generic-vlc","displayName":"VLC media player","version":"3.0.21.0","scope":"machine","success":"true","lastExitCode":"0"}}
@@ -0,0 +1,9 @@
# RealmJoinPackageStarted v1 — observation-only per-package event.
# Posted by RealmJoinWatcherAdapter when a HKLM\SOFTWARE\RealmJoin\Packages\<id> sub-key
# (or its HKU\<sid>\... counterpart) is first observed with a DisplayName value.
# Payload: packageId (mandatory), displayName (truncated to 256 chars), optional version,
# scope ("machine"|"user").
# Reducer: HandleRealmJoinPackageStartedV1 → appends a RealmJoinPackageFact row. Stage
# unchanged. Dual emission as InformationalEvent realmjoin_package_started handles UI
# timeline visibility.
{"SessionSignalOrdinal":7,"SessionTraceOrdinal":7,"Kind":"RealmJoinPackageStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-21T10:05:00.0000000Z","SourceOrigin":"RealmJoinWatcher","Evidence":{"Kind":"Derived","Identifier":"realmjoin-package-started:machine:generic-vlc","Summary":"RealmJoin package install started (scope=machine, id=generic-vlc)","DerivationInputs":{"registryKey":"HKLM\\SYSTEM\\CurrentControlSet\\Services\\realmjoin\\Parameters"}},"Payload":{"packageId":"generic-vlc","displayName":"VLC media player","version":"3.0.21.0","scope":"machine"}}
@@ -0,0 +1,8 @@
# RealmJoinResolved v1 — RealmJoin first deployment completed.
# Posted by RealmJoinWatcherAdapter when DeploymentPhase first reaches
# CompletedFirstDeployment (110). Payload carries the observed phase.
# Reducer: HandleRealmJoinResolvedV1 → records RealmJoinFacts.ResolvedUtc/LastPhase/Outcome,
# cancels the RealmJoinTimeout deadline (scheduler + reducer view), and (if Hello + Desktop
# already resolved OR SelfDeployingDeferredCompletion is set) routes through
# CompleteIfDeferredOrBookkeep to TransitionToFinalizing / direct Completed.
{"SessionSignalOrdinal":42,"SessionTraceOrdinal":42,"Kind":"RealmJoinResolved","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-21T10:25:00.0000000Z","SourceOrigin":"RealmJoinWatcher","Evidence":{"Kind":"Derived","Identifier":"realmjoin-resolved-v1","Summary":"RealmJoin reached phase 110","DerivationInputs":{"registryKey":"HKLM\\SYSTEM\\CurrentControlSet\\Services\\realmjoin\\Parameters"}},"Payload":{"deploymentPhase":"110"}}
+8
View File
@@ -0,0 +1,8 @@
# RealmJoin hard-timeout (60-min) deadline fire.
# This kind is NOT posted by any adapter — it is dispatched by the DeadlineFiredV1 switch
# (DecisionEngine.Shared.cs) when the RealmJoinTimeout ActiveDeadline armed by
# HandleRealmJoinDetectedV1 elapses without DeploymentPhase reaching 110.
# Reducer: HandleRealmJoinTimeoutDeadlineFired → sets RealmJoinFacts.Outcome="Timeout",
# emits a realmjoin_timeout timeline event, and (if Hello+Desktop already resolved OR
# SelfDeployingDeferredCompletion is set) routes through CompleteIfDeferredOrBookkeep.
{"SessionSignalOrdinal":99,"SessionTraceOrdinal":99,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-21T11:00:00.0000000Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Derived","Identifier":"deadline-realmjoin-timeout","Summary":"RealmJoin hard timeout fired"},"Payload":{"deadline":"realmjoin_timeout"}}