From d1b4ceb5610f79cfc8ad14a20be82a26bcb4fc2c Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Thu, 23 Apr 2026 13:05:29 +0200 Subject: [PATCH] v2 single-rail 4.4: add architecture baseline test for emitter callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Living documentation of every type in V2.Core that structurally depends on TelemetryEventEmitter today. Shrinks with each migration PR; PR #10 tightens the assertion to the PR10 permitted set (EventTimelineEmitter + BackPressureEventObserver), at which point a new bypass attempt fails the build. - Reflection over fields, properties, constructor params, and method params on types in AutopilotMonitor.Agent.V2.Core. IL-level call-site detection is deliberately avoided: structural dependencies are sufficient to prove reachability and need no extra packages. - ExpectedCurrentBaseline enumerates the four known dependents today: Rail A (EventTimelineEmitter), the meta exception (BackPressureEventObserver), the EnrollmentOrchestrator field + EventEmitter property, and StartupEnvironmentProbes' static methods. - Second test asserts the PR10 permitted set is already a subset of the current baseline — guards against accidentally losing Rail A or the meta exception while migrating. - Program.cs is in the V2 executable assembly, outside this test's scope; it reaches the emitter via orchestrator.EventEmitter, which PR #10 internalizes. PR #0 is now functionally complete: InformationalEvent signal + reducer case + emitter parameter overrides + helper + baseline test, all with dedicated coverage. Build green, 624 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...lemetryEventEmitterCallersBaselineTests.cs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs new file mode 100644 index 00000000..7e57796d --- /dev/null +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Architecture/TelemetryEventEmitterCallersBaselineTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Agent.V2.Core.Telemetry.Events; +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). + /// + /// 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. + /// + /// + /// Detection strategy: reflection over fields, constructor parameters, method + /// parameters, and property return types. IL-level call-site detection is deliberately + /// avoided — structural dependencies are sufficient to guarantee a type has any way to + /// reach the emitter, and the reflection approach is cheap and dependency-free. + /// + /// + public sealed class TelemetryEventEmitterCallersBaselineTests + { + 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). + /// + private static readonly HashSet PermittedCallersAfterPr10 = 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 + Program.cs direct calls). Removed / internalized in PR #10. + "AutopilotMonitor.Agent.V2.Core.Orchestration.EnrollmentOrchestrator", + // Static RunAsync takes the emitter as a parameter to emit geo / timezone / ntp events. + // Migrated to ISignalIngressSink in PR #2. + "AutopilotMonitor.Agent.V2.Core.Runtime.StartupEnvironmentProbes", + }; + + [Fact] + public void V2Core_types_with_structural_dependency_on_TelemetryEventEmitter_match_current_baseline() + { + 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); + } + } + + private static ISet FindStructuralDependents(Assembly assembly) + { + const BindingFlags AllMembers = + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.Static | + BindingFlags.DeclaredOnly; + + var dependents = new HashSet(StringComparer.Ordinal); + + foreach (var type in assembly.GetTypes()) + { + if (type == EmitterType) continue; + if (type.FullName == null) continue; + // Compiler-generated display classes / iterators show up with '<' in the name; skip. + if (type.FullName.Contains("<")) continue; + + if (HasFieldOfType(type, EmitterType, AllMembers) + || HasPropertyOfType(type, EmitterType, AllMembers) + || HasMethodOrCtorParameterOfType(type, EmitterType, AllMembers)) + { + dependents.Add(type.FullName); + } + } + + return dependents; + } + + private static bool HasFieldOfType(Type type, Type target, BindingFlags flags) => + type.GetFields(flags).Any(f => f.FieldType == target); + + private static bool HasPropertyOfType(Type type, Type target, BindingFlags flags) => + type.GetProperties(flags).Any(p => p.PropertyType == target); + + private static bool HasMethodOrCtorParameterOfType(Type type, Type target, BindingFlags flags) + { + foreach (var method in type.GetMethods(flags)) + { + if (method.GetParameters().Any(p => p.ParameterType == target)) return true; + } + foreach (var ctor in type.GetConstructors(flags)) + { + if (ctor.GetParameters().Any(p => p.ParameterType == target)) return true; + } + return false; + } + } +}