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:
Oliver Kieselbach
2026-04-20 16:48:29 +02:00
parent 5a858cb4c5
commit 03b27e1377
2 changed files with 286 additions and 0 deletions
@@ -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,
});
}
}
}