Files
LlamaCasty/ytLive.Tests/StepDownPlannerTests.cs
T
gramps 555ff5c0ac feat(33): structured pump stats + the session planner and its plain-words copy
Slice 2. 588 green, 0 warnings.

- `FramePumpStats` and `FramePump.StatsReported`: the 5 s window the log line already describes, as
  data. `Dropped` is the ENCODER's counter (frames submitted, not accepted); `Stalls`/`AvgRenderMs`
  are the pump's. `FramesRendered` is deliberately not used for classification — a static scene
  reuses the paste cache and renders few frames while completely healthy, so it would read as
  starvation.
- `StepDownPlanner`: one per broadcast, owns the stateful policy, converts stats → sample, exposes the
  landed tier for the restart. It never acts — the VM owns the effects, which is what keeps the ladder
  testable without an encoder.
- `Describe` / `ReportLine`: the sentences a creator sees. Machine cause says LlamaCasty couldn't keep
  up and the connection was not the problem; upload cause blames the upload and promises no further
  thrash. A test asserts the machine copy never leaks "dropped" — mechanism strings stay in the log.

Remaining for the task: VM subscription, the encoder restart at the landed tier, the toast, and
shedding our own capture cost before stepping fps.
2026-11-09 10:06:36 -08:00

103 lines
4.0 KiB
C#

using System;
using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Encoder;
namespace ytLive.Tests;
/// <summary>
/// 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.
/// </summary>
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 < 4 && !decision.Acted; 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);
}
}
}