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