mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
refactor(v2core): M4.3.3 signal adapter (provisioning-status tracker)
Plan §2.1a / §2.2 — ProvisioningStatusTracker mit 2 Events auf 2 DecisionSignalKinds.
New:
- ProvisioningStatusTrackerAdapter
- DeviceSetupProvisioningComplete → DeviceSetupProvisioningComplete
(liest GetProvisioningCategorySnapshot() — deviceSetupResolved in Payload)
- EspFailureDetected → EspTerminalFailure (SourceOrigin unterscheidet von
ShellCoreTracker — wichtig für Multi-Source-Attribution im Journal)
Dual-Source-EspTerminalFailure-Handling (ShellCore + ProvisioningStatus):
Duplicate Signals sind erwartbar und OK — der Reducer akzeptiert die erste,
weitere landen als Dead-End-Transitions im Journal. Adapter-Dedup ist
tracker-lokal (pro Instanz).
Tests (+5, 107 total V2.Core.Tests):
- DeviceSetupComplete → DeviceSetupProvisioningComplete Signal
- EspFailure → EspTerminalFailure mit distinct SourceOrigin
- Empty failureType → "unknown"
- Per-kind dedup
- Null-args throw
Testsuite: 1223 → 1228.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
|
||||
using AutopilotMonitor.Agent.V2.Core.SignalAdapters;
|
||||
using AutopilotMonitor.Agent.V2.Core.Tests.Harness;
|
||||
using AutopilotMonitor.Agent.V2.Core.Tests.Orchestration;
|
||||
using AutopilotMonitor.DecisionCore.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);
|
||||
Tracker = new ProvisioningStatusTracker(
|
||||
sessionId: "S1",
|
||||
tenantId: "T1",
|
||||
onEventCollected: _ => { },
|
||||
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<ArgumentNullException>(
|
||||
() => new ProvisioningStatusTrackerAdapter(null!, f.Ingress, f.Clock));
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => new ProvisioningStatusTrackerAdapter(f.Tracker, null!, f.Clock));
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => new ProvisioningStatusTrackerAdapter(f.Tracker, f.Ingress, null!));
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
#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
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapter for <see cref="ProvisioningStatusTracker"/> → 2 DecisionSignalKinds.
|
||||
/// Plan §2.1a / §2.2.
|
||||
/// <para>
|
||||
/// Event mapping:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>DeviceSetupProvisioningComplete</c> → <see cref="DecisionSignalKind.DeviceSetupProvisioningComplete"/></item>
|
||||
/// <item><c>EspFailureDetected</c> → <see cref="DecisionSignalKind.EspTerminalFailure"/></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class ProvisioningStatusTrackerAdapter : IDisposable
|
||||
{
|
||||
private readonly ProvisioningStatusTracker _tracker;
|
||||
private readonly ISignalIngressSink _ingress;
|
||||
private readonly IClock _clock;
|
||||
|
||||
private bool _deviceSetupCompletePosted;
|
||||
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.EspFailureDetected += OnEspFailure;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_tracker.DeviceSetupProvisioningComplete -= OnDeviceSetupComplete;
|
||||
_tracker.EspFailureDetected -= OnEspFailure;
|
||||
}
|
||||
|
||||
private void OnDeviceSetupComplete(object sender, EventArgs e) => EmitDeviceSetupComplete();
|
||||
private void OnEspFailure(object sender, string failureType) => EmitEspFailure(failureType);
|
||||
|
||||
internal void TriggerDeviceSetupCompleteFromTest() => EmitDeviceSetupComplete();
|
||||
internal void TriggerEspFailureFromTest(string failureType) => EmitEspFailure(failureType);
|
||||
|
||||
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<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["registrySource"] = @"HKLM\SOFTWARE\Microsoft\Provisioning\AutopilotSettings\DeviceSetupCategory.Status",
|
||||
["deviceSetupResolved"] = deviceSetupResolved,
|
||||
}),
|
||||
payload: new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["deviceSetupResolved"] = deviceSetupResolved,
|
||||
});
|
||||
}
|
||||
|
||||
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: "ProvisioningStatusTracker",
|
||||
evidence: new Evidence(
|
||||
kind: EvidenceKind.Derived,
|
||||
identifier: "provisioning-status-tracker-v1",
|
||||
summary: $"ESP terminal failure from provisioning registry (type={safeFailureType})",
|
||||
derivationInputs: new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["registrySource"] = @"HKLM\SOFTWARE\Microsoft\Provisioning\AutopilotSettings\*.Status",
|
||||
["failureType"] = safeFailureType,
|
||||
}),
|
||||
payload: new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["failureType"] = safeFailureType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user