v2 single-rail 4.4: add architecture baseline test for emitter callers

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) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-04-23 13:05:29 +02:00
parent 428b14cd60
commit d1b4ceb561
@@ -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
{
/// <summary>
/// Single-rail refactor plan §4.4 / §8 — baseline inventory of every type in
/// <c>AutopilotMonitor.Agent.V2.Core</c> that can reach
/// <see cref="TelemetryEventEmitter"/>. 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 — <see cref="EventTimelineEmitter"/>
/// (Rail A) and <see cref="BackPressureEventObserver"/> (documented meta exception).
/// <para>
/// <b>Scope</b>: this test inspects the V2.Core assembly only. Program.cs lives in the
/// V2 executable assembly and reaches the emitter via
/// <see cref="EnrollmentOrchestrator.EventEmitter"/>; 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.
/// </para>
/// <para>
/// <b>Detection strategy</b>: 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.
/// </para>
/// </summary>
public sealed class TelemetryEventEmitterCallersBaselineTests
{
private static readonly Type EmitterType = typeof(TelemetryEventEmitter);
/// <summary>
/// Types permitted to reach <see cref="TelemetryEventEmitter"/> 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:
/// <c>Assert.Equal(PermittedCallers, ActualCallers)</c>.
/// </summary>
private static readonly HashSet<string> PermittedCallersAfterPr10 = new HashSet<string>(StringComparer.Ordinal)
{
"AutopilotMonitor.Agent.V2.Core.Telemetry.Events.EventTimelineEmitter",
"AutopilotMonitor.Agent.V2.Core.Telemetry.Events.BackPressureEventObserver",
};
/// <summary>
/// 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.
/// </summary>
private static readonly HashSet<string> ExpectedCurrentBaseline = new HashSet<string>(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<string> FindStructuralDependents(Assembly assembly)
{
const BindingFlags AllMembers =
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly;
var dependents = new HashSet<string>(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;
}
}
}