mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
refactor(v2core): M4.3.2 signal adapter (shell-core tracker)
Plan §2.1a / §2.2 — ShellCoreTracker verbindet 3 Events mit 3 DecisionSignalKinds. New: - ShellCoreTrackerAdapter - FinalizingSetupPhaseTriggered → EspPhaseChanged (phase=FinalizingSetup, reason propagated) - WhiteGloveCompleted → WhiteGloveShellCoreSuccess - EspFailureDetected → EspTerminalFailure (failureType in payload, "unknown" fallback) - Per-kind Dedup-Flags (independent fire-once pro Signal-Kind) NOTE: ModernDeploymentTracker ist NICHT Signal-relevant (emittiert EnrollmentEvents via onEventCollected callback → Events-Tabelle-Pipeline). Wird in M4.4 Orchestrator direkt an TelemetryTransport als Kind=Event gebridged, nicht als SignalAdapter. Event 509 (WhiteGlove Start) war in Legacy Scoring-Factor (+15); per M3-Erkenntnis wurde das Weight in V2 entfernt → keine Signal-Emission nötig. Tests (+7, 102 total V2.Core.Tests): - Finalizing → EspPhaseChanged mit korrekter EnrollmentPhase.FinalizingSetup - WhiteGloveCompleted → WhiteGloveShellCoreSuccess - EspFailure → EspTerminalFailure mit failureType - Empty failureType → "unknown" fallback - Per-kind independent dedup (3 Events → 3 Posts, Duplicates gefiltert) - All-3-in-one-session flow - Null-args throw Testsuite: 1216 → 1223. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
|
||||
using AutopilotMonitor.Agent.V2.Core.SignalAdapters;
|
||||
using AutopilotMonitor.Agent.V2.Core.Tests.Harness;
|
||||
using AutopilotMonitor.Agent.V2.Core.Tests.Orchestration;
|
||||
using AutopilotMonitor.DecisionCore.Engine;
|
||||
using AutopilotMonitor.DecisionCore.Signals;
|
||||
using AutopilotMonitor.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);
|
||||
Tracker = new ShellCoreTracker(
|
||||
sessionId: "S1",
|
||||
tenantId: "T1",
|
||||
onEventCollected: _ => { },
|
||||
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<ArgumentNullException>(() => new ShellCoreTrackerAdapter(null!, f.Ingress, f.Clock));
|
||||
Assert.Throws<ArgumentNullException>(() => new ShellCoreTrackerAdapter(f.Tracker, null!, f.Clock));
|
||||
Assert.Throws<ArgumentNullException>(() => new ShellCoreTrackerAdapter(f.Tracker, f.Ingress, null!));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
#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
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapter for <see cref="ShellCoreTracker"/> — translates its 3 events into Decision-Signals.
|
||||
/// Plan §2.1a / §2.2.
|
||||
/// <para>
|
||||
/// Event mapping (Plan §2.2 DecisionSignalKind):
|
||||
/// <list type="bullet">
|
||||
/// <item><c>FinalizingSetupPhaseTriggered</c> → <see cref="DecisionSignalKind.EspPhaseChanged"/> (phase=Finalizing)</item>
|
||||
/// <item><c>WhiteGloveCompleted</c> → <see cref="DecisionSignalKind.WhiteGloveShellCoreSuccess"/></item>
|
||||
/// <item><c>EspFailureDetected</c> → <see cref="DecisionSignalKind.EspTerminalFailure"/></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Jeder Signaltyp ist fire-once (ShellCoreTracker selbst hat Dedup-Guards; der Adapter
|
||||
/// führt zusätzlich pro-Kind-Flag-Dedup, damit doppelte Subscribe-Calls oder externe
|
||||
/// Re-Entry-Szenarien nicht zweimal posten).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_tracker.FinalizingSetupPhaseTriggered -= OnFinalizing;
|
||||
_tracker.WhiteGloveCompleted -= OnWhiteGloveCompleted;
|
||||
_tracker.EspFailureDetected -= OnEspFailure;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
internal void TriggerFinalizingFromTest(string reason) => EmitFinalizing(reason);
|
||||
internal void TriggerWhiteGloveCompletedFromTest() => EmitWhiteGloveSuccess();
|
||||
internal void TriggerEspFailureFromTest(string failureType) => EmitEspFailure(failureType);
|
||||
|
||||
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<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["eventSource"] = "Microsoft-Windows-Shell-Core",
|
||||
["phaseReason"] = reason ?? string.Empty,
|
||||
}),
|
||||
payload: new Dictionary<string, string>(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<string, string>(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<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["eventSource"] = "Microsoft-Windows-Shell-Core",
|
||||
["failureType"] = safeFailureType,
|
||||
}),
|
||||
payload: new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["failureType"] = safeFailureType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user