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))