diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs index 31adef37..e9426513 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs @@ -9,18 +9,17 @@ using Xunit; namespace AutopilotMonitor.Agent.V2.Core.Tests.Architecture { /// - /// Single-rail refactor plan §4.4 / §8 — baseline inventory of every type in - /// AutopilotMonitor.Agent.V2.Core that can reach - /// . The test is a living document: as the migration PRs - /// (#1 through #9) remove bypass paths one by one, the baseline shrinks. PR #10 tightens - /// the expected set to only the two permitted callers — - /// (Rail A) and (documented meta exception). + /// Single-rail refactor plan §4.4 / §5.10 / §8 — enforcement gate on the + /// caller set. PR #10 promoted this test from a + /// transitional baseline (3 dependents) to strict enforcement (exactly 2 permitted callers): + /// (Rail A) and + /// (documented meta exception — it cannot route through the ingress without creating a + /// cycle because it observes the ingress itself). /// - /// Scope: this test inspects the V2.Core assembly only. Program.cs lives in the - /// V2 executable assembly and reaches the emitter via - /// ; internalizing that property in PR #10 - /// removes the reach automatically. Until then Program.cs is tracked via the plan's §2 - /// inventory (B2–B6), not this test. + /// Scope: this test inspects the V2.Core assembly only. The emitter class itself is + /// internal post-PR #10, so there is no surface for the V2 executable assembly or + /// any external library to reach it — the inventory is complete with the two types that + /// live next to it in the Telemetry.Events namespace. /// /// /// Detection strategy: reflection over fields, constructor parameters, method @@ -34,55 +33,24 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Architecture private static readonly Type EmitterType = typeof(TelemetryEventEmitter); /// - /// Types permitted to reach at the end of the - /// single-rail refactor (PR #10 enforcement gate). Kept separate from the transitional - /// baseline so that a single-line diff in PR #10 switches enforcement on: - /// Assert.Equal(PermittedCallers, ActualCallers). + /// The two and only two types permitted to reach + /// after the single-rail refactor. A third entry would mean someone reintroduced a + /// bypass; the test fails and reviewers are forced to reason about whether the new + /// caller belongs here (essentially never — the answer is almost always "route via + /// ISignalIngressSink + InformationalEventPost instead"). /// - private static readonly HashSet PermittedCallersAfterPr10 = new HashSet(StringComparer.Ordinal) + private static readonly HashSet PermittedCallers = new HashSet(StringComparer.Ordinal) { "AutopilotMonitor.Agent.V2.Core.Telemetry.Events.EventTimelineEmitter", "AutopilotMonitor.Agent.V2.Core.Telemetry.Events.BackPressureEventObserver", }; - /// - /// Current transitional baseline (PR #0). Every additional entry is a documented - /// bypass that a later PR will remove. Adding a new bypass requires updating this list - /// explicitly — the review signal is loud because tests fail. - /// - private static readonly HashSet ExpectedCurrentBaseline = new HashSet(StringComparer.Ordinal) - { - // Rail A — engine effect path. Permitted after PR #10. - "AutopilotMonitor.Agent.V2.Core.Telemetry.Events.EventTimelineEmitter", - // Meta exception — cannot route through the ingress without a cycle. Permitted after PR #10. - "AutopilotMonitor.Agent.V2.Core.Telemetry.Events.BackPressureEventObserver", - // Owns the field and exposes it via the public EventEmitter property (BuildTelemetryEventSink - // factory + the ServerActionDispatcher emitEvent callback in Program.cs). Removed / - // internalized in PR #10 once those bypasses are themselves migrated. - "AutopilotMonitor.Agent.V2.Core.Orchestration.EnrollmentOrchestrator", - // PR #2 (5.2) migrated StartupEnvironmentProbes to InformationalEventPost — baseline - // shrunk by one. Next to fall: the ServerActionDispatcher / EnrollmentTerminationHandler - // callbacks, Periodic collectors, Enrollment SystemSignals trackers, Gather rules, - // Analyzers, DeviceInfoCollector (PR #3 – PR #9). - }; - [Fact] - public void V2Core_types_with_structural_dependency_on_TelemetryEventEmitter_match_current_baseline() + public void V2Core_types_with_structural_dependency_on_TelemetryEventEmitter_match_permitted_callers() { var actual = FindStructuralDependents(EmitterType.Assembly); - Assert.Equal(ExpectedCurrentBaseline.OrderBy(x => x), actual.OrderBy(x => x)); - } - - [Fact] - public void Current_baseline_is_a_superset_of_the_PR10_permitted_callers() - { - // Sanity guard: every permitted-after-PR10 caller must be present today, otherwise - // we've accidentally removed Rail A or the meta exception. - foreach (var permitted in PermittedCallersAfterPr10) - { - Assert.Contains(permitted, ExpectedCurrentBaseline); - } + Assert.Equal(PermittedCallers.OrderBy(x => x), actual.OrderBy(x => x)); } private static ISet FindStructuralDependents(Assembly assembly) diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorEventBridgeTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorEventBridgeTests.cs index 67b6eac8..41450411 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorEventBridgeTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorEventBridgeTests.cs @@ -1,50 +1,45 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Agent.V2.Core.Tests.Harness; using AutopilotMonitor.Agent.V2.Core.Tests.Transport; -using AutopilotMonitor.Agent.V2.Core.Transport.Telemetry; using AutopilotMonitor.DecisionCore.Classifiers; using AutopilotMonitor.DecisionCore.Engine; -using AutopilotMonitor.Shared.Models; using Xunit; #pragma warning disable xUnit1031 namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration { + /// + /// Orchestrator ↔ lifecycle wiring. Plan §5.10 (single-rail + /// enforcement): the factory no longer receives an Action<EnrollmentEvent> sink + /// — collectors post via . These tests assert Start creates + /// hosts + starts them, Stop stops + disposes them, and the factory receives the ingress + + /// clock it needs to wire its own posts. + /// public sealed class EnrollmentOrchestratorEventBridgeTests { private static DateTime At => new DateTime(2026, 4, 20, 10, 0, 0, DateTimeKind.Utc); - /// Test host that lets a test manually fire enrollment events. private sealed class FakeCollectorHost : ICollectorHost { - private readonly Action _sink; public string Name { get; } public int StartCalls { get; private set; } public int StopCalls { get; private set; } public int DisposeCalls { get; private set; } - public FakeCollectorHost(string name, Action sink) - { - Name = name; - _sink = sink; - } + public FakeCollectorHost(string name) { Name = name; } public void Start() => StartCalls++; public void Stop() => StopCalls++; public void Dispose() => DisposeCalls++; - - public void Emit(EnrollmentEvent evt) => _sink(evt); } private sealed class FakeComponentFactory : IComponentFactory { - public Action? CapturedSink { get; private set; } public readonly List Hosts = new List(); public readonly IReadOnlyList HostNames; @@ -56,7 +51,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration } public IReadOnlyCollection? CapturedWhiteGloveSealingPatternIds { get; private set; } - public ISignalIngressSink? CapturedIngress { get; private set; } public IClock? CapturedClock { get; private set; } @@ -64,18 +58,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration string sessionId, string tenantId, AgentLogger logger, - Action onEnrollmentEvent, IReadOnlyCollection whiteGloveSealingPatternIds, ISignalIngressSink ingress, IClock clock) { - CapturedSink = onEnrollmentEvent; CapturedWhiteGloveSealingPatternIds = whiteGloveSealingPatternIds; CapturedIngress = ingress; CapturedClock = clock; foreach (var name in HostNames) { - Hosts.Add(new FakeCollectorHost(name, onEnrollmentEvent)); + Hosts.Add(new FakeCollectorHost(name)); } return Hosts; } @@ -117,17 +109,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration public void Dispose() => Tmp.Dispose(); } - private static EnrollmentEvent Evt(string eventType) => new EnrollmentEvent - { - EventType = eventType, - Severity = EventSeverity.Info, - Source = "test", - Phase = EnrollmentPhase.Unknown, - Timestamp = At, - }; - - // ========================================================================= Wiring - [Fact] public void Start_without_factory_does_not_throw_and_spawns_no_hosts() { @@ -152,14 +133,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration } [Fact] - public void Start_with_factory_creates_all_hosts_and_captures_sink() + public void Start_with_factory_creates_all_hosts_and_starts_them() { using var rig = new Rig(); var sut = rig.Build(); sut.Start(); - Assert.NotNull(rig.Factory.CapturedSink); Assert.Equal(5, rig.Factory.Hosts.Count); Assert.All(rig.Factory.Hosts, h => Assert.Equal(1, h.StartCalls)); @@ -167,87 +147,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration } [Fact] - public void Emit_from_host_reaches_telemetry_transport() + public void Factory_receives_ingress_and_clock_from_orchestrator() { using var rig = new Rig(); var sut = rig.Build(); + sut.Start(); - var hello = rig.Factory.Hosts.Find(h => h.Name == "HelloTracker")!; - hello.Emit(Evt("hello_completed")); - - var spool = GetSpool(sut); - var pending = spool.Peek(10); - Assert.Single(pending); - Assert.Equal(TelemetryItemKind.Event, pending[0].Kind); - Assert.Contains("hello_completed", pending[0].PayloadJson); - - sut.Stop(); - } - - [Fact] - public void Emit_from_each_of_five_hosts_produces_five_transport_items() - { - using var rig = new Rig(); - var sut = rig.Build(); - sut.Start(); - - foreach (var host in rig.Factory.Hosts) - { - host.Emit(Evt($"event_from_{host.Name}")); - } - - var pending = GetSpool(sut).Peek(10); - Assert.Equal(5, pending.Count); - foreach (var host in rig.Factory.Hosts) - { - Assert.Contains(pending, p => p.PayloadJson.Contains($"event_from_{host.Name}")); - } - - sut.Stop(); - } - - [Fact] - public void Modern_deployment_host_emits_event_bridge_only_no_decision_signal() - { - using var rig = new Rig(); - var sut = rig.Build(); - sut.Start(); - - var modern = rig.Factory.Hosts.Find(h => h.Name == "ModernDeploymentTracker")!; - modern.Emit(Evt("modern_deployment_log")); - - // Event reached transport (bridge works). - var pending = GetSpool(sut).Peek(10); - Assert.Single(pending); - Assert.Contains("modern_deployment_log", pending[0].PayloadJson); - - // No decision-signal was posted — signal-log is empty (no SignalAdapter wraps - // ModernDeploymentTracker by design, Plan §4.x M4.3-Erkenntnis). - var signalLog = GetSignalLog(sut); - Assert.Empty(signalLog.ReadAll()); - - sut.Stop(); - } - - [Fact] - public void Emitter_exception_does_not_propagate_to_host_thread() - { - // Simulate: TelemetryEventEmitter throws (e.g. transport disposed mid-emit). - // Build a shared sink that throws on demand; wrap into a host that uses it directly. - // Easier path: dispose the transport post-Start so Enqueue throws, then fire an event. - using var rig = new Rig(); - var sut = rig.Build(); - sut.Start(); - - // Force a transport-level exception by disposing it mid-run. - GetTransport(sut).Dispose(); - - var hello = rig.Factory.Hosts.Find(h => h.Name == "HelloTracker")!; - - // Must not throw — orchestrator wraps the emitter call in try/catch. - var ex = Record.Exception(() => hello.Emit(Evt("hello_after_dispose"))); - Assert.Null(ex); + Assert.NotNull(rig.Factory.CapturedIngress); + Assert.NotNull(rig.Factory.CapturedClock); + Assert.Same(rig.Clock, rig.Factory.CapturedClock); sut.Stop(); } @@ -264,31 +173,5 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration Assert.All(rig.Factory.Hosts, h => Assert.Equal(1, h.StopCalls)); Assert.All(rig.Factory.Hosts, h => Assert.Equal(1, h.DisposeCalls)); } - - // ========================================================================= Helpers - - private static TelemetrySpool GetSpool(EnrollmentOrchestrator sut) - { - var field = typeof(EnrollmentOrchestrator).GetField( - "_spool", - System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - return (TelemetrySpool)field!.GetValue(sut)!; - } - - private static TelemetryUploadOrchestrator GetTransport(EnrollmentOrchestrator sut) - { - var field = typeof(EnrollmentOrchestrator).GetField( - "_transport", - System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - return (TelemetryUploadOrchestrator)field!.GetValue(sut)!; - } - - private static AutopilotMonitor.Agent.V2.Core.Persistence.ISignalLogWriter GetSignalLog(EnrollmentOrchestrator sut) - { - var field = typeof(EnrollmentOrchestrator).GetField( - "_signalLog", - System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - return (AutopilotMonitor.Agent.V2.Core.Persistence.ISignalLogWriter)field!.GetValue(sut)!; - } } } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorOnIngressReadyTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorOnIngressReadyTests.cs index a6338464..417e0a2d 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorOnIngressReadyTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorOnIngressReadyTests.cs @@ -138,7 +138,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration // Post-Start, observability accessors work (= Start completed, did not throw out). Assert.NotNull(orchestrator.IngressSink); - Assert.NotNull(orchestrator.EventEmitter); } [Fact] diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorTests.cs index 69ed392b..b4732426 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorTests.cs @@ -93,7 +93,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration var sut = rig.Build(); Assert.Throws(() => sut.CurrentState); Assert.Throws(() => sut.IngressSink); - Assert.Throws(() => sut.EventEmitter); // Regression guard: Program.WireTelemetryServerResponse must run AFTER Start(). // Touching Transport pre-Start threw InvalidOperationException and crashed the agent // on every VM after the first-run-ghost-detect fix let it reach this far. @@ -123,7 +122,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration Assert.Equal("S1", sut.CurrentState.SessionId); Assert.Equal("T1", sut.CurrentState.TenantId); Assert.NotNull(sut.IngressSink); - Assert.NotNull(sut.EventEmitter); Assert.False(sut.IsQuarantineRequested); sut.Stop(); diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorWhiteGlovePatternTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorWhiteGlovePatternTests.cs index 5663a9b2..3a9d9a8c 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorWhiteGlovePatternTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/EnrollmentOrchestratorWhiteGlovePatternTests.cs @@ -24,7 +24,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration string sessionId, string tenantId, AgentLogger logger, - Action onEnrollmentEvent, IReadOnlyCollection whiteGloveSealingPatternIds, ISignalIngressSink ingress, IClock clock) diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs index 9ef4ecea..32821de9 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs @@ -91,7 +91,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration string sessionId, string tenantId, AgentLogger logger, - Action onEnrollmentEvent, IReadOnlyCollection whiteGloveSealingPatternIds, ISignalIngressSink ingress, IClock clock) @@ -99,7 +98,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration if (string.IsNullOrEmpty(sessionId)) throw new ArgumentException("SessionId required.", nameof(sessionId)); if (string.IsNullOrEmpty(tenantId)) throw new ArgumentException("TenantId required.", nameof(tenantId)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - if (onEnrollmentEvent == null) throw new ArgumentNullException(nameof(onEnrollmentEvent)); if (ingress == null) throw new ArgumentNullException(nameof(ingress)); if (clock == null) throw new ArgumentNullException(nameof(clock)); @@ -148,7 +146,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration _imeLogHost = new ImeLogHost( sessionId: sessionId, tenantId: tenantId, - onEnrollmentEvent: onEnrollmentEvent, logger: logger, ingress: ingress, clock: clock, @@ -518,7 +515,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration public ImeLogHost( string sessionId, string tenantId, - Action onEnrollmentEvent, AgentLogger logger, ISignalIngressSink ingress, IClock clock, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs index cd08baf6..d2bc503e 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs @@ -93,7 +93,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration private EventSequenceCounter? _eventSequenceCounter; private TelemetrySpool? _spool; private TelemetryUploadOrchestrator? _transport; - private TelemetryEventEmitter? _eventEmitter; private TelemetrySignalEmitter? _signalEmitter; private TelemetryTransitionEmitter? _transitionEmitter; private EventTimelineEmitter? _timelineEmitter; @@ -222,10 +221,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration public ISignalIngressSink IngressSink => (ISignalIngressSink?)_ingress ?? throw new InvalidOperationException("Orchestrator not started."); - /// Exposed für Sub-c-Wiring (Collector onEventCollected-Bridge). - public TelemetryEventEmitter EventEmitter => - _eventEmitter ?? throw new InvalidOperationException("Orchestrator not started."); - /// /// Exposed so Program.cs can subscribe to /// for M4.6.ε DeviceBlocked / DeviceKillSignal / AdminAction / Actions plumbing. @@ -321,13 +316,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration _spool = new TelemetrySpool(_transportDirectory, _clock); _transport = new TelemetryUploadOrchestrator(_spool, _uploader, _clock); - // 4) Event-Emitter-Kette. + // 4) Event-Emitter-Kette. Single-rail (Plan §5.10): der TelemetryEventEmitter wird + // lokal gebaut und nur in die zwei erlaubten Caller (EventTimelineEmitter, + // BackPressureEventObserver) injiziert. Kein Feld mehr auf dem Orchestrator — + // strukturelle Abhängigkeit erlischt damit, Architektur-Baseline-Test gate + // shrinks to exactly two permitted callers. _eventSequenceCounter = new EventSequenceCounter(_eventSequencePersistence); - _eventEmitter = new TelemetryEventEmitter(_transport, _eventSequenceCounter, _sessionId, _tenantId); + var eventEmitter = new TelemetryEventEmitter(_transport, _eventSequenceCounter, _sessionId, _tenantId); _signalEmitter = new TelemetrySignalEmitter(_transport, _sessionId, _tenantId); _transitionEmitter = new TelemetryTransitionEmitter(_transport, _sessionId, _tenantId); - _timelineEmitter = new EventTimelineEmitter(_eventEmitter); - _backPressureObserver = new BackPressureEventObserver(_eventEmitter, _clock); + _timelineEmitter = new EventTimelineEmitter(eventEmitter); + _backPressureObserver = new BackPressureEventObserver(eventEmitter, _clock); // 5) Deadlines + Classifiers. _scheduler = new DeadlineScheduler(_clock); @@ -441,14 +440,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration } } - // 14) Collector-Hosts via Factory — onEnrollmentEvent bridged Telemetry-Events - // an den zentralen TelemetryEventEmitter. Exception-Swallow damit ein - // Collector-Thread nicht am Emitter-Fehler stirbt (Plan §2.7a). + // 14) Collector-Hosts via Factory — nach Plan §5.10 (single-rail enforcement) gibt + // es keine Action-Bridge mehr; jede Collector-Emission fließt + // über den Ingress als InformationalEvent. if (_componentFactory != null) { - var safeSink = BuildTelemetryEventSink(); _collectorHosts = _componentFactory.CreateCollectorHosts( - _sessionId, _tenantId, _logger, safeSink, _whiteGloveSealingPatternIds, + _sessionId, _tenantId, _logger, _whiteGloveSealingPatternIds, ingress: _ingress, clock: _clock); foreach (var host in _collectorHosts) { @@ -667,27 +665,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration if (!Directory.Exists(_transportDirectory)) Directory.CreateDirectory(_transportDirectory); } - /// - /// Shared callback that collector hosts use for onEventCollected. Exception-safe: - /// an emitter failure (disk hiccup, transport closed) must never kill a collector thread. - /// - private Action BuildTelemetryEventSink() - { - return evt => - { - if (evt == null || _eventEmitter == null) return; - try - { - _eventEmitter.Emit(evt); - } - catch (Exception ex) - { - _logger.Error( - $"EnrollmentOrchestrator: telemetry emit failed for event '{evt.EventType}'.", ex); - } - }; - } - private void OnDeadlineFired(object? sender, DeadlineFiredEventArgs e) { if (Volatile.Read(ref _stopRequested) == 1 || _ingress == null) return; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/ICollectorHost.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/ICollectorHost.cs index 2f8cbdd8..ae4c8b10 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/ICollectorHost.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/ICollectorHost.cs @@ -5,14 +5,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration { /// /// Lifecycle-Abstraktion für einen einzelnen Collector im V2-Runtime. - /// Plan §2.1 / §4.x M4.4.5.c. + /// Plan §2.1 / §4.x M4.4.5.c / §5.10 (single-rail enforcement). /// /// Ein Host wrappt einen konkreten Collector (z.B. HelloTracker, /// ShellCoreTracker, ModernDeploymentTracker) und kapselt Start/Stop. - /// Die Action<EnrollmentEvent>-Callback für onEventCollected wird bei - /// der Host-Erstellung über die injiziert — der - /// Orchestrator übergibt dort - /// als Sink. + /// Nach PR #10 posten Collectors ihre Events ausschließlich als + /// InformationalEvent-Signals über den geteilten , + /// der über die bei der Host-Erstellung injiziert wird. /// /// /// SignalAdapter-Wiring: separat — kann die diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/IComponentFactory.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/IComponentFactory.cs index fd60fd64..eb97635e 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/IComponentFactory.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/IComponentFactory.cs @@ -1,53 +1,37 @@ #nullable enable -using System; using System.Collections.Generic; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.DecisionCore.Engine; -using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Orchestration { /// - /// Factory-Seam für Collector-Hosts. Plan §4.x M4.4.5.c + M4.5.b. + /// Factory-Seam für Collector-Hosts. Plan §4.x M4.4.5.c / §5.10 (single-rail enforcement). /// /// Der delegiert an diese Factory, weil die konkreten /// Collector-Ctoren heavy Production-Deps (Event-Log-Watcher, WMI, Registry) haben — Tests - /// liefern eine Fake-Factory, Prod liefert eine Default-Implementation, die HelloTracker, - /// ShellCoreTracker, ProvisioningStatusTracker, StallProbeCollector und - /// ModernDeploymentTracker baut und deren onEventCollected-Callbacks an den - /// übergebenen bindet. + /// liefern eine Fake-Factory, Prod liefert eine Default-Implementation, die die Tracker + /// + Hosts baut. Nach PR #10 gibt es keinen Action<EnrollmentEvent>-Parameter + /// mehr — alle Collectors posten via (Signal-Rail) statt + /// direkt an den . /// /// - /// ModernDeploymentTracker-Spezifikum: der einzige Kollektor ohne SignalAdapter — - /// bridged nur Events, keine DecisionSignals. Default-Factory-Impls dürfen ihn aufbauen, - /// Tests brauchen ihn nicht zwingend. + /// wird an den ImeLogTrackerAdapter + /// durchgereicht (Sealing-Emission fire-once nur für diese Pattern-IDs). Leer / null + /// = Feature off, M3-kompatibel. Plan §4.x M4.4.5.e. /// /// public interface IComponentFactory { /// - /// Baut alle Collector-Hosts und bindet deren Event-Emission an - /// (typischerweise - /// ). - /// Exceptions im Callback darf die Factory NICHT weiterreichen — der Orchestrator - /// wrappt das mit einem Exception-Swallow, damit ein Collector-Thread nicht stirbt. - /// - /// wird an den ImeLogTrackerAdapter - /// durchgereicht (Sealing-Emission fire-once nur für diese Pattern-IDs). Leer / null - /// = Feature off, M3-kompatibel. Plan §4.x M4.4.5.e. - /// - /// - /// M4.5.b-Extension: und werden - /// durchgereicht, damit die Default-Factory SignalAdapter direkt mitwiren kann (Adapters - /// brauchen einen + , die erst im - /// Orchestrator gebaut werden). - /// + /// Baut alle Collector-Hosts. Jeder Host ist selbst dafür zuständig, seine + /// InformationalEventPost aus (ingress, clock) zu konstruieren und Collector-Events + /// als InformationalEvent-Signals über zu posten. /// IReadOnlyList CreateCollectorHosts( string sessionId, string tenantId, AgentLogger logger, - Action onEnrollmentEvent, IReadOnlyCollection whiteGloveSealingPatternIds, ISignalIngressSink ingress, IClock clock); diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs index d457aadf..6c0e2bc5 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs @@ -16,7 +16,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Telemetry.Events /// — hier wird jedes reingereichte Ereignis emittiert. /// /// - public sealed class BackPressureEventObserver : IBackPressureObserver + internal sealed class BackPressureEventObserver : IBackPressureObserver { internal const string EventType = "ingress_backpressure"; internal const string SourceId = "signal_ingress"; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/EventTimelineEmitter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/EventTimelineEmitter.cs index 9ab2c8ab..66651f19 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/EventTimelineEmitter.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/EventTimelineEmitter.cs @@ -25,7 +25,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Telemetry.Events /// throw on a malformed phase value. /// /// - public sealed class EventTimelineEmitter : IEventTimelineEmitter + internal sealed class EventTimelineEmitter : IEventTimelineEmitter { /// Default when no override is provided. internal const string SourceId = "decision_engine"; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/TelemetryEventEmitter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/TelemetryEventEmitter.cs index ebdabef1..ad9ef7eb 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/TelemetryEventEmitter.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/TelemetryEventEmitter.cs @@ -8,13 +8,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Telemetry.Events { /// /// Zentrale Senke für alle -Emissionen im V2-Agent. - /// Plan §2.7a / L.10. + /// Plan §2.7a / L.10 / §5.10 (single-rail enforcement). /// - /// Drei Aufrufer: (Reducer-Effekte), - /// Collector-Callbacks (via Orchestrator-Wiring in M4.4.5) und - /// . Alle nutzen denselben Pfad: - /// Sequence-Vergabe → Wire-Format-Serialisierung (Newtonsoft, Legacy-kompatibel) → - /// Kind=Event → . + /// Nach PR #10 (single-rail enforcement) sind nur noch zwei Aufrufer erlaubt: + /// (Rail A, Reducer-Effekt-Pfad) und + /// (Meta-Ausnahme wegen zirkulärer + /// Ingress-Abhängigkeit). Beide wohnen im gleichen Namespace — der Typ ist + /// internal, sodass ein dritter Caller nur aus demselben Namespace + /// kompilieren kann (plus Test-Assembly via InternalsVisibleTo). /// /// /// Mutation: setzt , , @@ -22,7 +23,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Telemetry.Events /// Caller darf das Event nach nicht weiter verwenden. /// /// - public sealed class TelemetryEventEmitter + internal sealed class TelemetryEventEmitter { private readonly ITelemetryTransport _transport; private readonly EventSequenceCounter _sequenceCounter; @@ -49,7 +50,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Telemetry.Events /// Weist Sequence zu, serialisiert im Legacy-Wire-Format und enqueued via Transport. /// Thread-safe (EventSequenceCounter + ITelemetryTransport.Enqueue beide unter Lock). /// - public TelemetryItem Emit(EnrollmentEvent evt) + internal TelemetryItem Emit(EnrollmentEvent evt) { if (evt == null) throw new ArgumentNullException(nameof(evt)); if (string.IsNullOrEmpty(evt.EventType))