using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Stability; namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Stability; [Trait("Category", "Unit")] public sealed class MemoryRecycleTests { [Fact] public async Task TierC_HardBreach_RequestsSupervisorRecycle() { var supervisor = new FakeSupervisor(); var recycle = new MemoryRecycle(DriverTier.C, supervisor, NullLogger.Instance); var requested = await recycle.HandleAsync(MemoryTrackingAction.HardBreach, 2_000_000_000, CancellationToken.None); requested.ShouldBeTrue(); supervisor.RecycleCount.ShouldBe(1); supervisor.LastReason.ShouldContain("hard-breach"); } [Theory] [InlineData(DriverTier.A)] [InlineData(DriverTier.B)] public async Task InProcessTier_HardBreach_NeverRequestsRecycle(DriverTier tier) { var supervisor = new FakeSupervisor(); var recycle = new MemoryRecycle(tier, supervisor, NullLogger.Instance); var requested = await recycle.HandleAsync(MemoryTrackingAction.HardBreach, 2_000_000_000, CancellationToken.None); requested.ShouldBeFalse("Tier A/B hard-breach logs a promotion recommendation only (decisions #74, #145)"); supervisor.RecycleCount.ShouldBe(0); } [Fact] public async Task TierC_WithoutSupervisor_HardBreach_NoOp() { var recycle = new MemoryRecycle(DriverTier.C, supervisor: null, NullLogger.Instance); var requested = await recycle.HandleAsync(MemoryTrackingAction.HardBreach, 2_000_000_000, CancellationToken.None); requested.ShouldBeFalse("no supervisor → no recycle path; action logged only"); } [Theory] [InlineData(DriverTier.A)] [InlineData(DriverTier.B)] [InlineData(DriverTier.C)] public async Task SoftBreach_NeverRequestsRecycle(DriverTier tier) { var supervisor = new FakeSupervisor(); var recycle = new MemoryRecycle(tier, supervisor, NullLogger.Instance); var requested = await recycle.HandleAsync(MemoryTrackingAction.SoftBreach, 1_000_000_000, CancellationToken.None); requested.ShouldBeFalse("soft-breach is surface-only at every tier"); supervisor.RecycleCount.ShouldBe(0); } [Theory] [InlineData(MemoryTrackingAction.None)] [InlineData(MemoryTrackingAction.Warming)] public async Task NonBreachActions_NoOp(MemoryTrackingAction action) { var supervisor = new FakeSupervisor(); var recycle = new MemoryRecycle(DriverTier.C, supervisor, NullLogger.Instance); var requested = await recycle.HandleAsync(action, 100_000_000, CancellationToken.None); requested.ShouldBeFalse(); supervisor.RecycleCount.ShouldBe(0); } private sealed class FakeSupervisor : IDriverSupervisor { public string DriverInstanceId => "fake-tier-c"; public int RecycleCount { get; private set; } public string? LastReason { get; private set; } public Task RecycleAsync(string reason, CancellationToken cancellationToken) { RecycleCount++; LastReason = reason; return Task.CompletedTask; } } }