using System;
using Xunit;
using ytLive.Models;
namespace ytLive.Tests;
///
/// TASK 33 — the step-down ladders as pure policy. The two rules that shaped the design and are easy
/// to undo by "tidying" later:
///
///
/// - Two levers, never mixed. Machine starvation buys less work (fps, then resolution);
/// upload congestion buys fewer bits. Resolution at an unchanged bitrate relieves no congestion, and
/// an fps drop at an unchanged bitrate relieves no upload.
/// - One-way by default. Climbing is a constructor flag; the ceiling is carried either way
/// so recovery is a policy change, not a rewrite.
///
///
public sealed class StepDownPolicyTests
{
private static StepDownSample Machine(double renderMs = 40, int stalls = 10) =>
new(WindowSeconds: 5, PumpStalls: stalls, PumpDrops: 0, EncoderDrops: 0,
AvgRenderMs: renderMs, AvgSubmitWaitMs: 1);
private static StepDownSample Upload(int encoderDrops = 30) =>
new(WindowSeconds: 5, PumpStalls: 0, PumpDrops: 0, EncoderDrops: encoderDrops,
AvgRenderMs: 1, AvgSubmitWaitMs: 2);
private static StepDownSample Clean() =>
new(WindowSeconds: 5, PumpStalls: 0, PumpDrops: 0, EncoderDrops: 0,
AvgRenderMs: 1, AvgSubmitWaitMs: 1);
/// Feeds windows every 5 s until the policy changes QUALITY; the capture-load shed is a
/// pre-step (no budget, nothing the viewer sees) so it is skipped here, and tested on its own.
private static StepDownDecision Run(StepDownPolicy policy, StepDownSample sample, ref double t, int maxWindows = 24)
{
var last = new StepDownDecision(StepCause.None, StepAction.None, null);
for (var i = 0; i < maxWindows; i++)
{
t += 5;
last = policy.Observe(sample, TimeSpan.FromSeconds(t));
if (last.Acted && last.Action != StepAction.ShedCaptureCadence) return last;
}
return last;
}
[Fact]
public void MachineStarvation_ShedsOurOwnCaptureCostBeforeLoweringQuality()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0);
var t = 0.0;
StepDownDecision first = default;
for (var i = 0; i < 6 && !first.Acted; i++)
{
t += 5;
first = policy.Observe(Machine(), TimeSpan.FromSeconds(t));
}
Assert.Equal(StepAction.ShedCaptureCadence, first.Action);
Assert.Equal(60, policy.Fps);
Assert.Equal(0, policy.StepsUsed); // shedding is not a quality step
Assert.Equal(1920, policy.Width);
}
[Fact]
public void MachineStarvation_LowersFrameRateBeforeAnythingElse()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0);
var t = 0.0;
var decision = Run(policy, Machine(), ref t);
Assert.Equal(StepCause.Machine, decision.Cause);
Assert.Equal(StepAction.LowerFrameRate, decision.Action);
Assert.Equal(30, policy.Fps);
Assert.Equal(8.0, policy.BitrateMbps); // bits were never the problem
Assert.Equal(1920, policy.Width);
}
[Fact]
public void MachineStarvationAtThirty_DropsResolutionNext()
{
var policy = new StepDownPolicy(vertical: false, startFps: 30, startBitrateMbps: 8.0);
var t = 0.0;
var decision = Run(policy, Machine(renderMs: 40), ref t);
Assert.Equal(StepAction.LowerResolution, decision.Action);
Assert.Equal(1280, policy.Width);
Assert.Equal(720, policy.Height);
Assert.Equal(30, policy.Fps);
}
[Fact]
public void MachineStarvationAtTheFloor_HoldsAndSaysSo()
{
var policy = new StepDownPolicy(vertical: false, startFps: 30, startBitrateMbps: 3.0);
var t = 0.0;
Run(policy, Machine(), ref t); // 1080p30 → 720p30
var t2 = t;
var next = Run(policy, Machine(renderMs: 90), ref t2);
Assert.False(next.Acted);
Assert.Contains("floor", next.Why ?? string.Empty);
Assert.Equal(1280, policy.Width);
}
[Fact]
public void UploadCongestion_StepsBitrateOnly_NeverGeometry()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0);
var t = 0.0;
var decision = Run(policy, Upload(encoderDrops: 30), ref t); // 30 / (60×5) = 10% loss
Assert.Equal(StepCause.Upload, decision.Cause);
Assert.Equal(StepAction.LowerBitrate, decision.Action);
Assert.Equal(7.2, policy.BitrateMbps); // 8.0 − max(0.5, 0.8)
Assert.Equal(60, policy.Fps);
Assert.Equal(1920, policy.Width);
}
[Fact]
public void UploadStep_IsSizedByLossAndStopsAtTheFloor()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 6.0);
var t = 0.0;
for (var i = 0; i < 8; i++)
{
t += 40; // clear the cooldown
policy.Observe(Upload(encoderDrops: 60), TimeSpan.FromSeconds(t)); // 20% loss
}
Assert.True(policy.BitrateMbps >= 3.0);
Assert.Equal(60, policy.Fps); // congested upload never buys an fps drop
Assert.Equal(1920, policy.Width);
}
[Fact]
public void UploadStep_StopsAtTheTiersYoutubeMinimumWhenOneIsGiven()
{
// 1080p60: YouTube's floor is 6 Mbps, not the historical 3. From 8 the ladder reaches it in two
// steps (8 → 6.4 → 6.0) and must then hold rather than cut into the minimum.
var floor = RateLadder.For(1920, 1080, 60).Minimum;
Assert.Equal(6.0, floor);
var policy = new StepDownPolicy(1920, 1080, 60, 8.0, floorBitrateMbps: floor);
var t = 0.0;
for (var i = 0; i < 12; i++)
{
t += 40;
policy.Observe(Upload(encoderDrops: 60), TimeSpan.FromSeconds(t)); // 20% loss
}
Assert.Equal(6.0, policy.BitrateMbps);
}
[Fact]
public void BothKindsOfEvidenceInOneWindow_DoesNothing()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0);
var t = 0.0;
var mixed = new StepDownSample(5, PumpStalls: 20, PumpDrops: 3, EncoderDrops: 40,
AvgRenderMs: 50, AvgSubmitWaitMs: 30);
var decision = Run(policy, mixed, ref t, maxWindows: 6);
Assert.False(decision.Acted);
Assert.Equal(StepCause.Ambiguous, decision.Cause);
Assert.Equal(60, policy.Fps);
Assert.Equal(8.0, policy.BitrateMbps);
}
[Fact]
public void StepBudget_IsThreeForAFullHdStart_AndOneForSevenTwenty()
{
var full = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0);
var hd = new StepDownPolicy(startWidth: 1280, startHeight: 720, startFps: 60, startBitrateMbps: 6.0);
var vertical = new StepDownPolicy(vertical: true, startFps: 60, startBitrateMbps: 8.0);
Assert.Equal(3, full.StepsAllowed);
Assert.Equal(1, hd.StepsAllowed);
Assert.Equal(1, vertical.StepsAllowed);
}
[Fact]
public void VerticalTier_HasNoResolutionStepOnlyFrameRateAndBits()
{
var policy = new StepDownPolicy(vertical: true, startFps: 60, startBitrateMbps: 8.0);
var t = 0.0;
var first = Run(policy, Machine(), ref t);
Assert.Equal(StepAction.LowerFrameRate, first.Action);
Assert.Equal(1080, policy.Width);
Assert.Equal(1920, policy.Height);
var t2 = t;
var second = Run(policy, Machine(), ref t2);
Assert.False(second.Acted); // one step on a vertical start, then done
}
[Fact]
public void Cooldown_StopsASecondStepImmediatelyAfterTheFirst()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0);
var t = 0.0;
Run(policy, Upload(encoderDrops: 30), ref t);
// Three more qualifying windows, still inside the 30 s cooldown.
var acted = false;
for (var i = 0; i < 3; i++)
{
t += 5;
if (policy.Observe(Upload(encoderDrops: 30), TimeSpan.FromSeconds(t)).Acted) acted = true;
}
Assert.False(acted);
Assert.Equal(7.2, policy.BitrateMbps);
}
[Fact]
public void Climbing_IsOffByDefault_EvenAfterMinutesOfCleanWindows()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0);
var t = 0.0;
Run(policy, Upload(encoderDrops: 30), ref t);
var lowered = policy.BitrateMbps;
for (var i = 0; i < 60; i++)
{
t += 5;
policy.Observe(Clean(), TimeSpan.FromSeconds(t));
}
Assert.Equal(lowered, policy.BitrateMbps);
Assert.Equal(1, policy.StepsUsed);
}
[Fact]
public void WithRecoveryEnabled_ALongCleanStretchClimbsOneStep()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0, allowClimb: true);
var t = 0.0;
Run(policy, Upload(encoderDrops: 30), ref t);
StepDownDecision raised = default;
for (var i = 0; i < 60 && !raised.Acted; i++)
{
t += 5;
raised = policy.Observe(Clean(), TimeSpan.FromSeconds(t));
}
Assert.Equal(StepAction.RaiseBitrate, raised.Action);
Assert.True(policy.BitrateMbps > 7.2);
Assert.True(policy.BitrateMbps <= 8.0);
Assert.Equal(0, policy.StepsUsed);
}
[Fact]
public void AClimbFollowedByARelapse_FreezesTheSessionAtTheLowValue()
{
var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0, allowClimb: true);
var t = 0.0;
Run(policy, Upload(encoderDrops: 30), ref t);
for (var i = 0; i < 60; i++)
{
t += 5;
if (policy.Observe(Clean(), TimeSpan.FromSeconds(t)).Acted) break;
}
var raisedTo = policy.BitrateMbps;
// The link gives way again shortly after the climb.
for (var i = 0; i < 60; i++)
{
t += 5;
if (policy.Observe(Upload(encoderDrops: 60), TimeSpan.FromSeconds(t)).Acted) break;
}
Assert.True(policy.Frozen);
// Frozen: a much longer clean stretch must not climb again.
var afterRelapse = policy.BitrateMbps;
for (var i = 0; i < 120; i++)
{
t += 5;
policy.Observe(Clean(), TimeSpan.FromSeconds(t));
}
Assert.Equal(afterRelapse, policy.BitrateMbps);
Assert.True(afterRelapse <= raisedTo);
}
}