Last slice. 590 green, 0 warnings. - **Shed before you charge the viewer.** On machine evidence the policy's first act is `ShedCaptureCadence`: `ScreenCaptureFrameSource` widens its conversion interval 10 ms → 33 ms (≈100 → 30 conversions/s) through a new `ScreenCaptureManager.ShedLoad`. The output tier, the declared fps and the viewer's picture are untouched — we stop paying for readbacks we were throwing away. It costs no step budget; if the starvation persists, the fps ladder runs on the next clean window. - The shed is reset on every stop path, so a cheap session cannot leave a later one capped. - **The session line is a Logs-tab line, not a session-report line.** The session report is the money summary (Super Chats, per currency); quality chatter there would bury the numbers it exists for. The cause-tagged line already lands in the log at the moment of the step. TASK 33 is complete: two ladders, budgets, floors, ambiguity, one-way with recovery behind a flag, the relaunch, the copy, and the cost shed. 18 tests pin the ladders, the copy and the restart.
103 lines
4.1 KiB
C#
103 lines
4.1 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 < 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);
|
|
}
|
|
}
|
|
}
|