using System; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// TASK 33 slice 2 — the adaptor between the pump's structured stats and the pure policy, plus the two /// sentences a creator ever sees. The copy is pinned here because "own the mistake" is a product rule, /// not a wording preference: a mechanism string in a toast is the bug. /// public sealed class StepDownPlannerTests { private static FramePumpStats Strained(double renderMs = 45, int stalls = 12, int dropped = 0) => new(WindowSeconds: 5, FramesSubmitted: 150, FramesRendered: 150, AvgRenderMs: renderMs, AvgSubmitMs: 1, AvgWaitMs: 14, WorstRenderMs: renderMs * 2, WorstSubmitMs: 2, Dropped: dropped, Stalls: stalls); [Fact] public void MachineEvidenceAfterThreeWindows_LowersTheFrameRateAndSaysWhoseFaultItWas() { var now = new DateTime(2026, 11, 9, 12, 0, 0, DateTimeKind.Utc); var planner = new StepDownPlanner( new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0), now); StepDownDecision decision = default; for (var i = 0; i < 20 && !(decision.Acted && decision.Action != StepAction.ShedCaptureCadence); i++) { now = now.AddSeconds(5); decision = planner.Observe(Strained(), now); } Assert.True(decision.Acted); Assert.Equal(StepAction.LowerFrameRate, decision.Action); var landed = planner.Current; Assert.Equal(30, landed.Fps); var copy = StepDownPlanner.Describe(decision, landed); Assert.Contains("LlamaCasty couldn't keep up", copy); Assert.Contains("connection wasn't the problem", copy); Assert.DoesNotContain("dropped", copy, StringComparison.OrdinalIgnoreCase); } [Fact] public void EncoderDrops_LowerTheBitrateAndTheCopyBlamesTheUpload() { var now = new DateTime(2026, 11, 9, 12, 0, 0, DateTimeKind.Utc); var planner = new StepDownPlanner( new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0), now); StepDownDecision decision = default; for (var i = 0; i < 4 && !decision.Acted; i++) { now = now.AddSeconds(5); decision = planner.Observe(Strained(renderMs: 1, stalls: 0, dropped: 30), now); } Assert.Equal(StepAction.LowerBitrate, decision.Action); var landed = planner.Current; Assert.Equal(60, landed.Fps); Assert.Equal(1920, landed.Width); Assert.True(landed.BitrateMbps < 8.0); var copy = StepDownPlanner.Describe(decision, landed); Assert.Contains("upload couldn't hold", copy); Assert.DoesNotContain("couldn't keep up", copy); } [Fact] public void TheSessionReportLine_NamesTheCauseInOnePlace() { var when = new DateTime(2026, 11, 9, 12, 4, 0); var machine = new StepDownDecision(StepCause.Machine, StepAction.LowerFrameRate, "frame rate 60 → 30"); var upload = new StepDownDecision(StepCause.Upload, StepAction.LowerBitrate, "8.0 → 7.2 Mbps on 10% loss"); Assert.Equal("12:04 quality stepped down — encoder (frame rate 60 → 30)", StepDownPlanner.ReportLine(when, machine)); Assert.Equal("12:04 quality stepped down — upload (8.0 → 7.2 Mbps on 10% loss)", StepDownPlanner.ReportLine(when, upload)); Assert.Equal(string.Empty, StepDownPlanner.ReportLine(when, new StepDownDecision(StepCause.None, StepAction.None, null))); } [Fact] public void ShortWindows_AreIgnored_SoAStartupBurstCannotTriggerAStep() { var now = new DateTime(2026, 11, 9, 12, 0, 0, DateTimeKind.Utc); var planner = new StepDownPlanner( new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0), now); var burst = Strained() with { WindowSeconds = 1 }; for (var i = 0; i < 20; i++) { now = now.AddSeconds(1); Assert.False(planner.Observe(burst, now).Acted); } } }