diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs index 9177b624..a09423db 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs @@ -16,17 +16,16 @@ using Xunit; namespace AutopilotMonitor.Agent.V2.Core.Tests.Integration { /// - /// Plan §EspExiting Adapter — wire-through verification on TWO topologies: - /// - /// Single-tracker wiring: ShellCoreTracker.EspExited → - /// ShellCoreTrackerAdapter → ingress (test-only scenario). - /// Production wiring: ShellCoreTracker.EspExited → inner re-raise on - /// EspAndHelloTracker.EspExited → EspAndHelloTrackerAdapter → ingress. - /// This is the path the live V2-Host uses; without the coordinator forward the - /// new EspExiting signal would never reach the engine in production. - /// - /// Both paths end with a reducer step that arms the HelloSafety deadline when - /// AccountSetup is already entered. + /// Plan §EspExiting Adapter — wire-through verification of the production EspExiting path: + /// ShellCoreTracker.HandleBackfillRecord → EspExited event → inner re-raise on + /// EspAndHelloTracker.EspExited → EspAndHelloTrackerAdapter → ingress → engine + /// reducer. This is the path the live V2-Host uses; without the coordinator forward the + /// EspExiting signal would never reach the engine in production. + /// + /// The first test isolates the tracker's EspExited contract (source-event timestamp + /// preserved through backfill); the coordinator tests drive the full production chain through + /// to the HelloSafety deadline when AccountSetup is already entered. + /// /// public sealed class ShellCoreToEngineEspExitingTests { @@ -35,12 +34,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Integration "CommercialOOBE_ESPProgress_Page_Exiting"; // matches EspExitingPattern in ShellCoreTracker [Fact] - public void Tracker_backfill_record_drives_adapter_to_post_EspExiting_signal() + public void Tracker_backfill_record_raises_EspExited_with_source_timestamp() { + // Tracker-level contract: HandleBackfillRecord raises EspExited carrying the source + // event time (not Clock.UtcNow). The EspExited→EspExiting signal mapping and SourceOrigin + // are covered by the coordinator tests below; here we isolate the backfill→event link + // that the production coordinator depends on. using var tmp = new TempDirectory(); var logger = new AgentLogger(tmp.Path, AgentLogLevel.Info); var clock = new VirtualClock(ClockNow); - var ingress = new FakeSignalIngressSink(); var trackerPost = new InformationalEventPost(new FakeSignalIngressSink(), clock); using var tracker = new ShellCoreTracker( sessionId: "S1", @@ -48,77 +50,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Integration post: trackerPost, logger: logger, helloTracker: null); - using var adapter = new ShellCoreTrackerAdapter(tracker, ingress, clock); - // Source time IS in the past relative to the clock — verify the adapter forwards - // the historical timestamp from the EventArgs rather than collapsing to Clock.UtcNow. + EspExitedEventArgs captured = null; + tracker.EspExited += (_, args) => captured = args; + + // Source time IS in the past relative to the clock — verify the tracker carries the + // historical timestamp on the event rather than collapsing to Clock.UtcNow. var sourceTime = ClockNow.AddMinutes(-2); tracker.HandleBackfillRecord(EspExitingDescription, sourceTime); - // One EspExiting on the ingress with the source timestamp preserved. - Assert.Contains(ingress.Posted, p => p.Kind == DecisionSignalKind.EspExiting); - var espExiting = ingress.Posted.FirstOrDefault(p => p.Kind == DecisionSignalKind.EspExiting)!; - Assert.Equal("ShellCoreTracker", espExiting.SourceOrigin); - Assert.Equal(sourceTime, espExiting.OccurredAtUtc); - } - - [Fact] - public void Tracker_backfill_then_reducer_step_arms_HelloSafety_when_AccountSetup_observed() - { - // End-to-end: real tracker event → adapter signal → engine reducer → - // HelloSafety deadline. Builds the seed state with AccountSetup pre-entered so the - // reducer's ShouldTransitionToAwaitingHello guard unlocks on the EspExiting forward. - using var tmp = new TempDirectory(); - var logger = new AgentLogger(tmp.Path, AgentLogLevel.Info); - var clock = new VirtualClock(ClockNow); - var ingress = new FakeSignalIngressSink(); - var trackerPost = new InformationalEventPost(new FakeSignalIngressSink(), clock); - using var tracker = new ShellCoreTracker( - sessionId: "S1", - tenantId: "T1", - post: trackerPost, - logger: logger, - helloTracker: null); - using var adapter = new ShellCoreTrackerAdapter(tracker, ingress, clock); - - var sourceTime = ClockNow.AddMinutes(-1); - tracker.HandleBackfillRecord(EspExitingDescription, sourceTime); - - var espExiting = ingress.Posted.FirstOrDefault(p => p.Kind == DecisionSignalKind.EspExiting); - Assert.NotNull(espExiting); - - // Build a Classic state already at AccountSetup AND with the strong post-AccountSetup - // gate satisfied (session 330f73f3 fix) so the EspExiting guard unlocks. Pre-fix this - // worked with only AccountSetupEnteredUtc; the new gate requires AccountSetupProvisioningSucceededUtc. - var engine = new DecisionEngine(); - var seed = DecisionState.CreateInitial("S1", "T1", agentBootUtc: sourceTime.AddMinutes(-5)) - .ToBuilder() - .WithStage(SessionStage.EspAccountSetup) - .WithStepIndex(2) - .WithLastAppliedSignalOrdinal(1); - seed.AccountSetupEnteredUtc = new SignalFact(sourceTime.AddMinutes(-2), 1); - seed.AccountSetupProvisioningSucceededUtc = new SignalFact(sourceTime.AddMinutes(-1).AddSeconds(-30), 1); - var state = seed.Build(); - - // Re-wrap the adapter-posted signal with the ordinal the engine expects. - var enrichedSignal = new DecisionSignal( - sessionSignalOrdinal: 2, - sessionTraceOrdinal: 2, - kind: espExiting!.Kind, - kindSchemaVersion: espExiting.KindSchemaVersion, - occurredAtUtc: espExiting.OccurredAtUtc, - sourceOrigin: espExiting.SourceOrigin, - evidence: espExiting.Evidence, - payload: espExiting.Payload); - - var step = engine.Reduce(state, enrichedSignal); - - Assert.Equal(SessionStage.AwaitingHello, step.NewState.Stage); - var helloSafety = Assert.Single(step.NewState.Deadlines, d => d.Name == DeadlineNames.HelloSafety); - Assert.Equal(sourceTime.AddSeconds(300), helloSafety.DueAtUtc); - Assert.Contains(step.Effects, - e => e.Kind == DecisionEffectKind.ScheduleDeadline - && e.Deadline?.Name == DeadlineNames.HelloSafety); + Assert.NotNull(captured); + Assert.Equal(sourceTime, captured!.OccurredAtUtc); } // -------------------------------------------- Coordinator-pfad (production wiring) -- diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspAndHelloTrackerAdapterTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspAndHelloTrackerAdapterTests.cs index 1033957c..41644290 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspAndHelloTrackerAdapterTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspAndHelloTrackerAdapterTests.cs @@ -57,6 +57,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters Assert.Equal("unknown", posted.Payload![SignalPayloadKeys.HelloOutcome]); } + [Theory] + [InlineData("completed")] + [InlineData("skipped")] + [InlineData("timeout")] + [InlineData("not_configured")] + [InlineData("wizard_not_started")] + public void HelloOutcome_propagates_verbatim_to_payload(string outcome) + { + using var f = new Fixture(); + using var adapter = new EspAndHelloTrackerAdapter(f.Coordinator, f.Ingress, f.Clock); + + adapter.TriggerHelloFromTest(outcome); + + Assert.Equal(outcome, f.Ingress.Posted[0].Payload![SignalPayloadKeys.HelloOutcome]); + } + [Fact] public void FinalizingEvent_emits_EspPhaseChanged_with_FinalizingSetup_payload() { @@ -97,6 +113,18 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters Assert.Contains("merged", posted.Evidence.DerivationInputs!["subSource"]); } + [Fact] + public void EspFailureEvent_empty_type_falls_back_to_unknown() + { + using var f = new Fixture(); + using var adapter = new EspAndHelloTrackerAdapter(f.Coordinator, f.Ingress, f.Clock); + + // Empty failureType is normalized to "unknown" by EspFailureDetectedEventArgs. + adapter.TriggerEspFailureFromTest(""); + + Assert.Equal("unknown", f.Ingress.Posted[0].Payload!["failureType"]); + } + [Fact] public void DeviceSetupCompleteEvent_emits_DeviceSetupProvisioningComplete_with_snapshot_fallback() { diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs index b9226098..67f36a06 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs @@ -104,40 +104,5 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters } finally { coordinator.Dispose(); } } - - // ------------------------------------------------------------------ ProvisioningStatusTrackerAdapter - - [Fact] - public void Provisioning_adapter_forwards_full_args_to_signal_payload() - { - using var tmp = new TempDirectory(); - var logger = new AgentLogger(tmp.Path, AgentLogLevel.Info); - var clock = new VirtualClock(Fixed); - var ingress = new FakeSignalIngressSink(); - var tracker = new ProvisioningStatusTracker( - sessionId: "S1", - tenantId: "T1", - post: new InformationalEventPost(new FakeSignalIngressSink(), clock), - logger: logger); - - try - { - using var adapter = new ProvisioningStatusTrackerAdapter(tracker, ingress, clock); - - adapter.TriggerEspFailureFromTest(new EspFailureDetectedEventArgs( - failureType: "Provisioning_DeviceSetup_Apps_Failed", - errorCode: "0x87d1041c", - failedSubcategory: "Apps", - category: "DeviceSetup")); - - var posted = Assert.Single(ingress.Posted); - Assert.Equal(DecisionSignalKind.EspTerminalFailure, posted.Kind); - Assert.Equal("Provisioning_DeviceSetup_Apps_Failed", posted.Payload!["failureType"]); - Assert.Equal("0x87d1041c", posted.Payload!["errorCode"]); - Assert.Equal("Apps", posted.Payload!["failedSubcategory"]); - Assert.Equal("DeviceSetup", posted.Payload!["category"]); - } - finally { tracker.Dispose(); } - } } } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/HelloTrackerAdapterTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/HelloTrackerAdapterTests.cs deleted file mode 100644 index 44416260..00000000 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/HelloTrackerAdapterTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Linq; -using AutopilotMonitor.Agent.V2.Core.Logging; -using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals; -using AutopilotMonitor.Agent.V2.Core.Orchestration; -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 Xunit; - -namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters -{ - public sealed class HelloTrackerAdapterTests - { - private static readonly DateTime Fixed = new DateTime(2026, 4, 20, 10, 0, 0, DateTimeKind.Utc); - - private sealed class Fixture : IDisposable - { - public TempDirectory Tmp { get; } = new TempDirectory(); - public AgentLogger Logger { get; } - public HelloTracker Tracker { get; } - public FakeSignalIngressSink Ingress { get; } = new FakeSignalIngressSink(); - public VirtualClock Clock { get; } = new VirtualClock(Fixed); - - public Fixture() - { - Logger = new AgentLogger(Tmp.Path, AgentLogLevel.Info); - // Separate throwaway ingress for tracker emissions so the adapter's DecisionSignal - // assertions on Ingress.Posted stay unpolluted by InformationalEvent pass-through. - var trackerPost = new InformationalEventPost(new FakeSignalIngressSink(), Clock); - Tracker = new HelloTracker( - sessionId: "S1", - tenantId: "T1", - post: trackerPost, - logger: Logger); - } - - public void Dispose() - { - Tracker.Dispose(); - Tmp.Dispose(); - } - } - - [Fact] - public void TriggerFromTest_emits_HelloResolved_with_outcome_in_payload() - { - using var f = new Fixture(); - using var adapter = new HelloTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFromTest("completed"); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(DecisionSignalKind.HelloResolved, posted.Kind); - Assert.Equal(Fixed, posted.OccurredAtUtc); - Assert.Equal("HelloTracker", posted.SourceOrigin); - Assert.Equal("completed", posted.Payload![SignalPayloadKeys.HelloOutcome]); - Assert.Contains("completed", posted.Evidence.Summary); - } - - [Theory] - [InlineData("completed")] - [InlineData("skipped")] - [InlineData("timeout")] - [InlineData("not_configured")] - [InlineData("wizard_not_started")] - public void Outcome_propagates_verbatim_to_payload(string outcome) - { - using var f = new Fixture(); - using var adapter = new HelloTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFromTest(outcome); - - Assert.Equal(outcome, f.Ingress.Posted[0].Payload![SignalPayloadKeys.HelloOutcome]); - } - - [Fact] - public void Null_or_empty_outcome_falls_back_to_unknown() - { - using var f = new Fixture(); - using var adapter = new HelloTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFromTest(null!); - - Assert.Equal("unknown", f.Ingress.Posted[0].Payload![SignalPayloadKeys.HelloOutcome]); - } - - [Fact] - public void Duplicate_trigger_is_deduplicated() - { - using var f = new Fixture(); - using var adapter = new HelloTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFromTest("completed"); - adapter.TriggerFromTest("timeout"); // even with different outcome, we're fire-once - - Assert.Single(f.Ingress.Posted); - Assert.Equal("completed", f.Ingress.Posted[0].Payload![SignalPayloadKeys.HelloOutcome]); - } - - [Fact] - public void Ctor_null_args_throw() - { - using var f = new Fixture(); - Assert.Throws(() => new HelloTrackerAdapter(null!, f.Ingress, f.Clock)); - Assert.Throws(() => new HelloTrackerAdapter(f.Tracker, null!, f.Clock)); - Assert.Throws(() => new HelloTrackerAdapter(f.Tracker, f.Ingress, null!)); - } - } -} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ProvisioningStatusTrackerAdapterTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ProvisioningStatusTrackerAdapterTests.cs deleted file mode 100644 index e3fb2526..00000000 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ProvisioningStatusTrackerAdapterTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Linq; -using AutopilotMonitor.Agent.V2.Core.Logging; -using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals; -using AutopilotMonitor.Agent.V2.Core.Orchestration; -using AutopilotMonitor.Agent.V2.Core.SignalAdapters; -using AutopilotMonitor.Agent.V2.Core.Tests.Harness; -using AutopilotMonitor.Agent.V2.Core.Tests.Orchestration; -using AutopilotMonitor.DecisionCore.Signals; -using Xunit; - -namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters -{ - public sealed class ProvisioningStatusTrackerAdapterTests - { - private static readonly DateTime Fixed = new DateTime(2026, 4, 20, 10, 0, 0, DateTimeKind.Utc); - - private sealed class Fixture : IDisposable - { - public TempDirectory Tmp { get; } = new TempDirectory(); - public AgentLogger Logger { get; } - public ProvisioningStatusTracker Tracker { get; } - public FakeSignalIngressSink Ingress { get; } = new FakeSignalIngressSink(); - public VirtualClock Clock { get; } = new VirtualClock(Fixed); - - public Fixture() - { - Logger = new AgentLogger(Tmp.Path, AgentLogLevel.Info); - var trackerPost = new InformationalEventPost(new FakeSignalIngressSink(), Clock); - Tracker = new ProvisioningStatusTracker( - sessionId: "S1", - tenantId: "T1", - post: trackerPost, - logger: Logger); - } - - public void Dispose() - { - Tracker.Dispose(); - Tmp.Dispose(); - } - } - - [Fact] - public void DeviceSetupComplete_maps_to_DeviceSetupProvisioningComplete_signal() - { - using var f = new Fixture(); - using var adapter = new ProvisioningStatusTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerDeviceSetupCompleteFromTest(); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(DecisionSignalKind.DeviceSetupProvisioningComplete, posted.Kind); - Assert.Equal(Fixed, posted.OccurredAtUtc); - Assert.Equal("ProvisioningStatusTracker", posted.SourceOrigin); - // Snapshot without real registry data → "unknown" - Assert.Equal("unknown", posted.Payload!["deviceSetupResolved"]); - } - - [Fact] - public void EspFailure_maps_to_EspTerminalFailure_with_distinct_sourceOrigin_from_ShellCore() - { - using var f = new Fixture(); - using var adapter = new ProvisioningStatusTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerEspFailureFromTest("DevicePreparation_Failed"); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(DecisionSignalKind.EspTerminalFailure, posted.Kind); - // SourceOrigin distinguishes ShellCore vs. ProvisioningStatus — important for - // multi-source failure attribution in the journal. - Assert.Equal("ProvisioningStatusTracker", posted.SourceOrigin); - Assert.Equal("DevicePreparation_Failed", posted.Payload!["failureType"]); - } - - [Fact] - public void EspFailure_empty_type_falls_back_to_unknown() - { - using var f = new Fixture(); - using var adapter = new ProvisioningStatusTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerEspFailureFromTest(""); - - Assert.Equal("unknown", f.Ingress.Posted[0].Payload!["failureType"]); - } - - [Fact] - public void Each_signal_kind_is_deduplicated_independently() - { - using var f = new Fixture(); - using var adapter = new ProvisioningStatusTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerDeviceSetupCompleteFromTest(); - adapter.TriggerDeviceSetupCompleteFromTest(); // dedup - adapter.TriggerEspFailureFromTest("t1"); - adapter.TriggerEspFailureFromTest("t2"); // dedup - - Assert.Equal(2, f.Ingress.Posted.Count); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.DeviceSetupProvisioningComplete); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.EspTerminalFailure); - } - - [Fact] - public void Ctor_null_args_throw() - { - using var f = new Fixture(); - Assert.Throws( - () => new ProvisioningStatusTrackerAdapter(null!, f.Ingress, f.Clock)); - Assert.Throws( - () => new ProvisioningStatusTrackerAdapter(f.Tracker, null!, f.Clock)); - Assert.Throws( - () => new ProvisioningStatusTrackerAdapter(f.Tracker, f.Ingress, null!)); - } - } -} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ShellCoreTrackerAdapterTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ShellCoreTrackerAdapterTests.cs deleted file mode 100644 index f3b141fb..00000000 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ShellCoreTrackerAdapterTests.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Linq; -using AutopilotMonitor.Agent.V2.Core.Logging; -using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals; -using AutopilotMonitor.Agent.V2.Core.Orchestration; -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.Shared.Models; -using Xunit; - -namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters -{ - public sealed class ShellCoreTrackerAdapterTests - { - private static readonly DateTime Fixed = new DateTime(2026, 4, 20, 10, 0, 0, DateTimeKind.Utc); - - private sealed class Fixture : IDisposable - { - public TempDirectory Tmp { get; } = new TempDirectory(); - public AgentLogger Logger { get; } - public ShellCoreTracker Tracker { get; } - public FakeSignalIngressSink Ingress { get; } = new FakeSignalIngressSink(); - public VirtualClock Clock { get; } = new VirtualClock(Fixed); - - public Fixture() - { - Logger = new AgentLogger(Tmp.Path, AgentLogLevel.Info); - var trackerPost = new InformationalEventPost(new FakeSignalIngressSink(), Clock); - Tracker = new ShellCoreTracker( - sessionId: "S1", - tenantId: "T1", - post: trackerPost, - logger: Logger, - helloTracker: null); - } - - public void Dispose() - { - Tracker.Dispose(); - Tmp.Dispose(); - } - } - - [Fact] - public void FinalizingPhase_maps_to_EspPhaseChanged_with_Finalizing_payload() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFinalizingFromTest("shell-core-62404"); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(DecisionSignalKind.EspPhaseChanged, posted.Kind); - Assert.Equal(Fixed, posted.OccurredAtUtc); - Assert.Equal("ShellCoreTracker", posted.SourceOrigin); - Assert.Equal( - EnrollmentPhase.FinalizingSetup.ToString(), - posted.Payload![SignalPayloadKeys.EspPhase]); - Assert.Equal("shell-core-62404", posted.Payload["reason"]); - } - - [Fact] - public void WhiteGloveCompleted_maps_to_WhiteGloveShellCoreSuccess() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerWhiteGloveCompletedFromTest(); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(DecisionSignalKind.WhiteGloveShellCoreSuccess, posted.Kind); - Assert.Equal("ShellCoreTracker", posted.SourceOrigin); - Assert.Equal(EvidenceKind.Derived, posted.Evidence.Kind); - Assert.Contains("sealing success", posted.Evidence.Summary); - } - - [Fact] - public void EspFailure_maps_to_EspTerminalFailure_with_failureType_in_payload() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerEspFailureFromTest("AccountSetup_Timeout"); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(DecisionSignalKind.EspTerminalFailure, posted.Kind); - Assert.Equal("AccountSetup_Timeout", posted.Payload!["failureType"]); - Assert.Equal("AccountSetup_Timeout", posted.Evidence.DerivationInputs!["failureType"]); - } - - [Fact] - public void EspFailure_empty_type_falls_back_to_unknown() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerEspFailureFromTest(""); - - Assert.Equal("unknown", f.Ingress.Posted[0].Payload!["failureType"]); - } - - [Fact] - public void Each_signal_kind_is_deduplicated_independently() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFinalizingFromTest("r1"); - adapter.TriggerFinalizingFromTest("r2"); // dedup - adapter.TriggerWhiteGloveCompletedFromTest(); - adapter.TriggerWhiteGloveCompletedFromTest(); // dedup - adapter.TriggerEspFailureFromTest("t1"); - adapter.TriggerEspFailureFromTest("t2"); // dedup - - Assert.Equal(3, f.Ingress.Posted.Count); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.EspPhaseChanged); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.WhiteGloveShellCoreSuccess); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.EspTerminalFailure); - } - - [Fact] - public void All_three_events_can_fire_in_one_session() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFinalizingFromTest("phase-enter"); - adapter.TriggerWhiteGloveCompletedFromTest(); - adapter.TriggerEspFailureFromTest("Timeout"); - - Assert.Equal(3, f.Ingress.Posted.Count); - Assert.Equal(DecisionSignalKind.EspPhaseChanged, f.Ingress.Posted[0].Kind); - Assert.Equal(DecisionSignalKind.WhiteGloveShellCoreSuccess, f.Ingress.Posted[1].Kind); - Assert.Equal(DecisionSignalKind.EspTerminalFailure, f.Ingress.Posted[2].Kind); - } - - [Fact] - public void Ctor_null_args_throw() - { - using var f = new Fixture(); - Assert.Throws(() => new ShellCoreTrackerAdapter(null!, f.Ingress, f.Clock)); - Assert.Throws(() => new ShellCoreTrackerAdapter(f.Tracker, null!, f.Clock)); - Assert.Throws(() => new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, null!)); - } - - // ---- EspExited → DecisionSignalKind.EspExiting (Plan §EspExiting Adapter) ---- - - [Fact] - public void EspExited_maps_to_EspExiting_signal() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - var sourceTime = new DateTime(2026, 5, 12, 17, 45, 30, DateTimeKind.Utc); - - adapter.TriggerEspExitingFromTest(sourceTime); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(DecisionSignalKind.EspExiting, posted.Kind); - Assert.Equal("ShellCoreTracker", posted.SourceOrigin); - Assert.Equal(EvidenceKind.Derived, posted.Evidence.Kind); - Assert.Contains("ESP exiting", posted.Evidence.Summary); - Assert.Equal("Microsoft-Windows-Shell-Core", posted.Evidence.DerivationInputs!["eventSource"]); - Assert.Equal("62407", posted.Evidence.DerivationInputs["eventId"]); - } - - [Fact] - public void EspExited_posts_every_occurrence_no_dedup() - { - // Shell-Core 62407 fires at every ESP phase transition (Device→Account, Account→End). - // The adapter must NOT dedup — the reducer (ShouldTransitionToAwaitingHello) - // distinguishes the genuine post-AccountSetup exit from intermediate ones. - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerEspExitingFromTest(new DateTime(2026, 5, 12, 17, 20, 0, DateTimeKind.Utc)); - adapter.TriggerEspExitingFromTest(new DateTime(2026, 5, 12, 17, 45, 0, DateTimeKind.Utc)); - adapter.TriggerEspExitingFromTest(new DateTime(2026, 5, 12, 18, 10, 0, DateTimeKind.Utc)); - - Assert.Equal(3, f.Ingress.Posted.Count); - Assert.All(f.Ingress.Posted, p => Assert.Equal(DecisionSignalKind.EspExiting, p.Kind)); - } - - [Fact] - public void EspExited_preserves_source_event_timestamp_not_clock_now() - { - // OccurredAtUtc must come from the source event (live = log time, backfill = - // record.TimeCreated), not from the adapter's clock — otherwise the engine's - // EffectiveDeadlineBase would floor HelloSafety at "now" on a backfilled run. - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - var pastTime = new DateTime(2026, 4, 1, 8, 15, 22, DateTimeKind.Utc); // before f.Clock = 2026-04-20 - - adapter.TriggerEspExitingFromTest(pastTime); - - var posted = Assert.Single(f.Ingress.Posted); - Assert.Equal(pastTime, posted.OccurredAtUtc); - Assert.NotEqual(f.Clock.UtcNow, posted.OccurredAtUtc); - } - - [Fact] - public void All_four_events_can_fire_in_one_session() - { - using var f = new Fixture(); - using var adapter = new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, f.Clock); - - adapter.TriggerFinalizingFromTest("phase-enter"); - adapter.TriggerWhiteGloveCompletedFromTest(); - adapter.TriggerEspFailureFromTest("Timeout"); - adapter.TriggerEspExitingFromTest(Fixed); - - Assert.Equal(4, f.Ingress.Posted.Count); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.EspPhaseChanged); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.WhiteGloveShellCoreSuccess); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.EspTerminalFailure); - Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.EspExiting); - } - } -} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs index d81bb736..742e8711 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs @@ -299,10 +299,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals catch (Exception ex) { _logger.Error($"EspFailureDetected handler failed for '{detectedFailureType}'", ex); } } - // Fire EspExited AFTER event emission. Subscribers (ShellCoreTrackerAdapter) post - // a DecisionSignalKind.EspExiting so the engine can arm HelloSafety on the genuine - // post-AccountSetup exit. Engine-side guard (ShouldTransitionToAwaitingHello) - // distinguishes intermediate exits from the real one. + // Fire EspExited AFTER event emission. The coordinator (EspAndHelloTracker) re-raises + // this and EspAndHelloTrackerAdapter posts a DecisionSignalKind.EspExiting so the + // engine can arm HelloSafety on the genuine post-AccountSetup exit. Engine-side guard + // (ShouldTransitionToAwaitingHello) distinguishes intermediate exits from the real one. if (eventType == Constants.EventTypes.EspExiting) { try { EspExited?.Invoke(this, new EspExitedEventArgs(timestamp)); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs index c088a5c0..6c79dd85 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs @@ -25,9 +25,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration /// EspAndHelloHost — coordinator /// (internally aggregates HelloTracker + ShellCoreTracker + ProvisioningStatusTracker /// + ModernDeploymentTracker) wired via . - /// This is the single production entry for the ESP+Hello signal surface — avoids - /// double emission that would happen if the sub-tracker adapters were also wired - /// in parallel (§4.x M4.3 tech-debt note about adapter duplication). + /// This is the single production entry for the ESP+Hello signal surface: the sub-trackers + /// are private to the coordinator and only their re-raised events are adapted here, so + /// there is exactly one DecisionSignal per source event (no double emission). /// DesktopArrivalHost — + . /// AadJoinHost — + . /// ImeLogHost — + diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/HelloTrackerAdapter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/HelloTrackerAdapter.cs deleted file mode 100644 index 1e384f02..00000000 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/HelloTrackerAdapter.cs +++ /dev/null @@ -1,73 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals; -using AutopilotMonitor.Agent.V2.Core.Orchestration; -using AutopilotMonitor.DecisionCore.Engine; -using AutopilotMonitor.DecisionCore.Signals; - -namespace AutopilotMonitor.Agent.V2.Core.SignalAdapters -{ - /// - /// Adapter for → . - /// Plan §2.1a / §2.2. - /// - /// Fires genau einmal, wenn der Tracker Hello-Abschluss feststellt. Payload enthält den - /// HelloOutcome-String (completed | skipped | timeout | not_configured | wizard_not_started), - /// der vom Reducer für die SignalFact<string> HelloOutcome-Fakt-Update verwendet wird. - /// - /// - internal sealed class HelloTrackerAdapter : IDisposable - { - private readonly HelloTracker _tracker; - private readonly ISignalIngressSink _ingress; - private readonly IClock _clock; - private bool _fired; - - public HelloTrackerAdapter( - HelloTracker tracker, - ISignalIngressSink ingress, - IClock clock) - { - _tracker = tracker ?? throw new ArgumentNullException(nameof(tracker)); - _ingress = ingress ?? throw new ArgumentNullException(nameof(ingress)); - _clock = clock ?? throw new ArgumentNullException(nameof(clock)); - - _tracker.HelloCompleted += OnHelloCompleted; - } - - public void Dispose() - { - _tracker.HelloCompleted -= OnHelloCompleted; - } - - private void OnHelloCompleted(object sender, EventArgs e) => EmitInternal(_tracker.HelloOutcome); - - internal void TriggerFromTest(string helloOutcome) => EmitInternal(helloOutcome); - - private void EmitInternal(string? helloOutcome) - { - if (_fired) return; - _fired = true; - - var outcome = string.IsNullOrEmpty(helloOutcome) ? "unknown" : helloOutcome!; - - _ingress.Post( - kind: DecisionSignalKind.HelloResolved, - occurredAtUtc: _clock.UtcNow, - sourceOrigin: "HelloTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "hello-tracker-v1", - summary: $"Hello resolved (outcome={outcome})", - derivationInputs: new Dictionary(StringComparer.Ordinal) - { - [SignalPayloadKeys.HelloOutcome] = outcome, - }), - payload: new Dictionary(StringComparer.Ordinal) - { - [SignalPayloadKeys.HelloOutcome] = outcome, - }); - } - } -} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ProvisioningStatusTrackerAdapter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ProvisioningStatusTrackerAdapter.cs deleted file mode 100644 index afe6d79f..00000000 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ProvisioningStatusTrackerAdapter.cs +++ /dev/null @@ -1,173 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals; -using AutopilotMonitor.Agent.V2.Core.Orchestration; -using AutopilotMonitor.DecisionCore.Engine; -using AutopilotMonitor.DecisionCore.Signals; - -namespace AutopilotMonitor.Agent.V2.Core.SignalAdapters -{ - /// - /// Adapter for → 3 DecisionSignalKinds. - /// Plan §2.1a / §2.2. - /// - /// Event mapping: - /// - /// DeviceSetupProvisioningComplete → - /// AccountSetupProvisioningComplete → (session 330f73f3 fix) - /// EspFailureDetected → - /// - /// - /// - /// Duplicate EspTerminalFailure-Signals aus ShellCoreTracker + ProvisioningStatusTracker - /// sind erwartbar (zwei Detection-Quellen für dasselbe Outcome). Der Reducer handled das - /// idempotent — erste Failure gewinnt und setzt Stage auf Failed; weitere sind dann Dead-End. - /// Adapter führt nur lokale per-kind Dedup (innerhalb einer Tracker-Instanz). - /// - /// - internal sealed class ProvisioningStatusTrackerAdapter : IDisposable - { - private readonly ProvisioningStatusTracker _tracker; - private readonly ISignalIngressSink _ingress; - private readonly IClock _clock; - - private bool _deviceSetupCompletePosted; - private bool _accountSetupCompletePosted; - private bool _espFailurePosted; - - public ProvisioningStatusTrackerAdapter( - ProvisioningStatusTracker tracker, - ISignalIngressSink ingress, - IClock clock) - { - _tracker = tracker ?? throw new ArgumentNullException(nameof(tracker)); - _ingress = ingress ?? throw new ArgumentNullException(nameof(ingress)); - _clock = clock ?? throw new ArgumentNullException(nameof(clock)); - - _tracker.DeviceSetupProvisioningComplete += OnDeviceSetupComplete; - _tracker.AccountSetupProvisioningComplete += OnAccountSetupComplete; - _tracker.EspFailureDetected += OnEspFailure; - } - - public void Dispose() - { - _tracker.DeviceSetupProvisioningComplete -= OnDeviceSetupComplete; - _tracker.AccountSetupProvisioningComplete -= OnAccountSetupComplete; - _tracker.EspFailureDetected -= OnEspFailure; - } - - private void OnDeviceSetupComplete(object sender, EventArgs e) => EmitDeviceSetupComplete(); - private void OnAccountSetupComplete(object sender, EventArgs e) => EmitAccountSetupComplete(); - private void OnEspFailure(object sender, EspFailureDetectedEventArgs args) => EmitEspFailure(args); - - internal void TriggerDeviceSetupCompleteFromTest() => EmitDeviceSetupComplete(); - internal void TriggerAccountSetupCompleteFromTest() => EmitAccountSetupComplete(); - internal void TriggerEspFailureFromTest(string failureType) - => EmitEspFailure(new EspFailureDetectedEventArgs(failureType)); - internal void TriggerEspFailureFromTest(EspFailureDetectedEventArgs args) => EmitEspFailure(args); - - private void EmitDeviceSetupComplete() - { - if (_deviceSetupCompletePosted) return; - _deviceSetupCompletePosted = true; - - var snapshot = _tracker.GetProvisioningCategorySnapshot(); - var deviceSetupResolved = snapshot.TryGetValue("DeviceSetup", out var dsState) && dsState.HasValue - ? dsState.Value.ToString().ToLowerInvariant() - : "unknown"; - - _ingress.Post( - kind: DecisionSignalKind.DeviceSetupProvisioningComplete, - occurredAtUtc: _clock.UtcNow, - sourceOrigin: "ProvisioningStatusTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "provisioning-status-tracker-v1", - summary: "DeviceSetupCategory provisioning completed", - derivationInputs: new Dictionary(StringComparer.Ordinal) - { - ["registrySource"] = @"HKLM\SOFTWARE\Microsoft\Provisioning\AutopilotSettings\DeviceSetupCategory.Status", - ["deviceSetupResolved"] = deviceSetupResolved, - }), - payload: new Dictionary(StringComparer.Ordinal) - { - ["deviceSetupResolved"] = deviceSetupResolved, - }); - } - - private void EmitAccountSetupComplete() - { - if (_accountSetupCompletePosted) return; - _accountSetupCompletePosted = true; - - var snapshot = _tracker.GetProvisioningCategorySnapshot(); - var accountSetupResolved = snapshot.TryGetValue("AccountSetupCategory.Status", out var asState) && asState.HasValue - ? asState.Value.ToString().ToLowerInvariant() - : "unknown"; - - _ingress.Post( - kind: DecisionSignalKind.AccountSetupProvisioningComplete, - occurredAtUtc: _clock.UtcNow, - sourceOrigin: "ProvisioningStatusTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "provisioning-status-tracker-v1", - summary: "AccountSetupCategory provisioning completed", - derivationInputs: new Dictionary(StringComparer.Ordinal) - { - ["registrySource"] = @"HKLM\SOFTWARE\Microsoft\Provisioning\AutopilotSettings\AccountSetupCategory.Status", - ["accountSetupResolved"] = accountSetupResolved, - }), - payload: new Dictionary(StringComparer.Ordinal) - { - ["accountSetupResolved"] = accountSetupResolved, - }); - } - - private void EmitEspFailure(EspFailureDetectedEventArgs args) - { - if (_espFailurePosted) return; - _espFailurePosted = true; - - var safeFailureType = args?.FailureType ?? "unknown"; - var errorCode = args?.ErrorCode; - var failedSubcategory = args?.FailedSubcategory; - var category = args?.Category; - - var derivationInputs = new Dictionary(StringComparer.Ordinal) - { - ["registrySource"] = @"HKLM\SOFTWARE\Microsoft\Provisioning\AutopilotSettings\*.Status", - ["failureType"] = safeFailureType, - }; - if (!string.IsNullOrEmpty(errorCode)) - derivationInputs["errorCode"] = errorCode!; - if (!string.IsNullOrEmpty(failedSubcategory)) - derivationInputs["failedSubcategory"] = failedSubcategory!; - if (!string.IsNullOrEmpty(category)) - derivationInputs["category"] = category!; - - var payload = new Dictionary(StringComparer.Ordinal) - { - ["failureType"] = safeFailureType, - }; - if (!string.IsNullOrEmpty(errorCode)) - payload["errorCode"] = errorCode!; - if (!string.IsNullOrEmpty(failedSubcategory)) - payload["failedSubcategory"] = failedSubcategory!; - if (!string.IsNullOrEmpty(category)) - payload["category"] = category!; - - _ingress.Post( - kind: DecisionSignalKind.EspTerminalFailure, - occurredAtUtc: _clock.UtcNow, - sourceOrigin: "ProvisioningStatusTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "provisioning-status-tracker-v1", - summary: $"ESP terminal failure from provisioning registry (type={safeFailureType}, errorCode={errorCode ?? "n/a"})", - derivationInputs: derivationInputs), - payload: payload); - } - } -} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ShellCoreTrackerAdapter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ShellCoreTrackerAdapter.cs deleted file mode 100644 index eabf1bd1..00000000 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ShellCoreTrackerAdapter.cs +++ /dev/null @@ -1,167 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals; -using AutopilotMonitor.Agent.V2.Core.Orchestration; -using AutopilotMonitor.DecisionCore.Engine; -using AutopilotMonitor.DecisionCore.Signals; -using AutopilotMonitor.Shared.Models; - -namespace AutopilotMonitor.Agent.V2.Core.SignalAdapters -{ - /// - /// Adapter for — translates its 4 events into Decision-Signals. - /// Plan §2.1a / §2.2. - /// - /// Event mapping (Plan §2.2 DecisionSignalKind): - /// - /// FinalizingSetupPhaseTriggered → (phase=Finalizing) - /// WhiteGloveCompleted → - /// EspFailureDetected → - /// EspExited → - /// - /// - /// - /// Finalizing / WhiteGloveSuccess / EspFailure are fire-once (ShellCoreTracker has Dedup-Guards; - /// the adapter adds per-kind flags so duplicate Subscribe-Calls or external re-entry can't - /// double-post). EspExiting is intentionally NOT deduped at the adapter — Shell-Core - /// 62407 fires at every ESP phase transition (Device→Account, Account→End), and the - /// reducer (HandleEspExitingV1 + ShouldTransitionToAwaitingHello) decides which occurrence - /// is the genuine post-AccountSetup exit that arms HelloSafety. - /// - /// - internal sealed class ShellCoreTrackerAdapter : IDisposable - { - private readonly ShellCoreTracker _tracker; - private readonly ISignalIngressSink _ingress; - private readonly IClock _clock; - - private bool _finalizingPosted; - private bool _whiteGloveSuccessPosted; - private bool _espFailurePosted; - - public ShellCoreTrackerAdapter( - ShellCoreTracker tracker, - ISignalIngressSink ingress, - IClock clock) - { - _tracker = tracker ?? throw new ArgumentNullException(nameof(tracker)); - _ingress = ingress ?? throw new ArgumentNullException(nameof(ingress)); - _clock = clock ?? throw new ArgumentNullException(nameof(clock)); - - _tracker.FinalizingSetupPhaseTriggered += OnFinalizing; - _tracker.WhiteGloveCompleted += OnWhiteGloveCompleted; - _tracker.EspFailureDetected += OnEspFailure; - _tracker.EspExited += OnEspExited; - } - - public void Dispose() - { - _tracker.FinalizingSetupPhaseTriggered -= OnFinalizing; - _tracker.WhiteGloveCompleted -= OnWhiteGloveCompleted; - _tracker.EspFailureDetected -= OnEspFailure; - _tracker.EspExited -= OnEspExited; - } - - private void OnFinalizing(object sender, string reason) => EmitFinalizing(reason); - private void OnWhiteGloveCompleted(object sender, EventArgs e) => EmitWhiteGloveSuccess(); - private void OnEspFailure(object sender, string failureType) => EmitEspFailure(failureType); - private void OnEspExited(object sender, EspExitedEventArgs args) => EmitEspExiting(args.OccurredAtUtc); - - internal void TriggerFinalizingFromTest(string reason) => EmitFinalizing(reason); - internal void TriggerWhiteGloveCompletedFromTest() => EmitWhiteGloveSuccess(); - internal void TriggerEspFailureFromTest(string failureType) => EmitEspFailure(failureType); - internal void TriggerEspExitingFromTest(DateTime occurredAtUtc) => EmitEspExiting(occurredAtUtc); - - private void EmitFinalizing(string reason) - { - if (_finalizingPosted) return; - _finalizingPosted = true; - - _ingress.Post( - kind: DecisionSignalKind.EspPhaseChanged, - occurredAtUtc: _clock.UtcNow, - sourceOrigin: "ShellCoreTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "shell-core-tracker-v1", - summary: "ESP Finalizing phase triggered (Shell-Core 62404/62407)", - derivationInputs: new Dictionary(StringComparer.Ordinal) - { - ["eventSource"] = "Microsoft-Windows-Shell-Core", - ["phaseReason"] = reason ?? string.Empty, - }), - payload: new Dictionary(StringComparer.Ordinal) - { - [SignalPayloadKeys.EspPhase] = EnrollmentPhase.FinalizingSetup.ToString(), - ["reason"] = reason ?? string.Empty, - }); - } - - private void EmitWhiteGloveSuccess() - { - if (_whiteGloveSuccessPosted) return; - _whiteGloveSuccessPosted = true; - - _ingress.Post( - kind: DecisionSignalKind.WhiteGloveShellCoreSuccess, - occurredAtUtc: _clock.UtcNow, - sourceOrigin: "ShellCoreTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "shell-core-tracker-v1", - summary: "WhiteGlove sealing success observed (Shell-Core)", - derivationInputs: new Dictionary(StringComparer.Ordinal) - { - ["eventSource"] = "Microsoft-Windows-Shell-Core", - ["detection"] = "WhiteGloveCompleted event", - })); - } - - private void EmitEspFailure(string failureType) - { - if (_espFailurePosted) return; - _espFailurePosted = true; - - var safeFailureType = string.IsNullOrEmpty(failureType) ? "unknown" : failureType!; - - _ingress.Post( - kind: DecisionSignalKind.EspTerminalFailure, - occurredAtUtc: _clock.UtcNow, - sourceOrigin: "ShellCoreTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "shell-core-tracker-v1", - summary: $"ESP terminal failure detected (type={safeFailureType})", - derivationInputs: new Dictionary(StringComparer.Ordinal) - { - ["eventSource"] = "Microsoft-Windows-Shell-Core", - ["failureType"] = safeFailureType, - }), - payload: new Dictionary(StringComparer.Ordinal) - { - ["failureType"] = safeFailureType, - }); - } - - // OccurredAtUtc comes from the tracker (live = log time, backfill = record.TimeCreated) - // rather than _clock.UtcNow so HandleEspExitingV1 can floor HelloSafety at the actual - // ESP-exit moment via EffectiveDeadlineBase, not at wall-clock-now on a backfilled run. - private void EmitEspExiting(DateTime occurredAtUtc) - { - _ingress.Post( - kind: DecisionSignalKind.EspExiting, - occurredAtUtc: occurredAtUtc, - sourceOrigin: "ShellCoreTracker", - evidence: new Evidence( - kind: EvidenceKind.Derived, - identifier: "shell-core-tracker-v1", - summary: "ESP exiting (Shell-Core 62407 OOBE_ESP*Exiting)", - derivationInputs: new Dictionary(StringComparer.Ordinal) - { - ["eventSource"] = "Microsoft-Windows-Shell-Core", - ["eventId"] = "62407", - })); - } - } -} diff --git a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt index b633d3d9..89a41372 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt @@ -1 +1 @@ -1036 +1039