Files
Flaro/Packages/FlixyKit/Tests/FlixyPlaybackTests/DowngradeRuleTests.swift
T
ScarriffleandClaude Opus 5 efed8717a3 „Auto" gibt nicht mehr auf, es versucht es kleiner
Der Fehlstart im Zug hat den eigentlichen Mangel gezeigt: die ganze neue
Regelung misst **ab dem ersten Bild**. Kommt nie eines, misst sie nichts,
greift nichts — und „Auto" hat sich als das erwiesen, was es wörtlich
heisst: „nimm, was in den Einstellungen steht". Steht dort „Original",
bittet die App über eine Zugverbindung um eine 4K-Datei und wartet
vierzig Sekunden auf etwas, das nie kommen kann.

Statt aufzugeben geht der Watchdog jetzt eine Sprosse tiefer und baut
neu auf — bei Filmen über `restart()`, bei Sendern über den Tuner-Abbau.
Gezählt gegen dasselbe Budget wie die Sicherheitsleine, sonst schickt
eine tote Leitung die App endlos im Kreis.

Scheitert auch das, nennt die Meldung jetzt den Grund und den Weg statt
nur „keine Daten mehr": wer ohne Grenze angefragt hat, soll erfahren,
dass es daran liegt und wo es sich ändern lässt.

Und „Strom" heisst Stream. Zweiunddreissig Stellen, dazu das
Gedächtnis ergänzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:30:10 +02:00

129 lines
4.7 KiB
Swift

import Testing
import Foundation
@testable import FlixyPlayback
@Suite("Wann der teure Weg erlaubt ist")
struct DowngradeRuleTests {
/// Der Normalfall, in dem abgesenkt werden soll: keine Leiter da, es
/// hakt dauerhaft, und es läuft lange genug.
private func notlage(
isAutomatic: Bool = true,
hasLadder: Bool = false,
isAtLowestRung: Bool = false,
condition: StreamCondition = .failing,
stalls: Int = 3,
longest: Duration = .seconds(4),
playedFor: Duration = .seconds(120),
sinceLastSeek: Duration? = nil,
sinceLastDowngrade: Duration? = nil,
used: Int = 0,
budget: Int = 2
) -> DowngradeRule.Facts {
.init(
isAutomatic: isAutomatic,
hasLadder: hasLadder,
isAtLowestRung: isAtLowestRung,
condition: condition,
stallsInWindow: stalls,
longestStallInWindow: longest,
playedFor: playedFor,
sinceLastSeek: sinceLastSeek,
sinceLastDowngrade: sinceLastDowngrade,
used: used,
budget: budget
)
}
@Test("Ohne Leiter und mit drei Rücklern: ja")
func downgradesWhenNoLadder() {
#expect(DowngradeRule.verdict(notlage()) == .downgrade)
}
/// Eine ausdrücklich gewählte Stufe ist eine Ansage. `automatic` ist die
/// Erlaubnis einzugreifen, alles andere nicht.
@Test("Eine gewählte Stufe wird nicht überstimmt")
func neverOverridesAChoice() {
#expect(DowngradeRule.verdict(notlage(isAutomatic: false)) == .notAutomatic)
}
/// Solange es noch eine Sprosse tiefer gibt, kostet der billige Hebel
/// kein schwarzes Bild.
@Test("Solange die Leiter trägt, bleibt es beim billigen Hebel")
func prefersTheCheapLever() {
let facts = notlage(hasLadder: true, isAtLowestRung: false)
#expect(DowngradeRule.verdict(facts) == .cheaperPathRemains)
}
@Test("Auf der untersten Sprosse ist der billige Hebel erschöpft")
func lowestRungAllowsTheExpensivePath() {
let facts = notlage(hasLadder: true, isAtLowestRung: true)
#expect(DowngradeRule.verdict(facts) == .downgrade)
}
@Test("Zwei Rückler genügen nicht")
func twoStallsAreNotEnough() {
#expect(DowngradeRule.verdict(notlage(stalls: 2)) == .notBadEnough)
}
/// Fünfzehn Sekunden Stillstand sind keine Schwankung. Darauf zu warten,
/// bis drei zusammenkommen, hieße viereinhalb Minuten zusehen.
@Test("Ein einzelner langer Rückler genügt allein")
func oneLongStallIsEnough() {
let facts = notlage(stalls: 1, longest: .seconds(20))
#expect(DowngradeRule.verdict(facts) == .downgrade)
}
@Test("Ein gesunder Stream löst nichts aus")
func healthyNeverTriggers() {
#expect(DowngradeRule.verdict(notlage(condition: .healthy)) == .notBadEnough)
}
/// Das anfängliche Füllen des Puffers sieht aus wie eine Notlage und ist
/// keine.
@Test("In den ersten Sekunden passiert nichts")
func waitsAfterStart() {
#expect(DowngradeRule.verdict(notlage(playedFor: .seconds(5))) == .tooEarly)
}
/// Ein Sprung leert den Puffer immer. Sich dafür zu bestrafen wäre
/// absurd.
@Test("Kurz nach einem Sprung passiert nichts")
func waitsAfterSeek() {
let facts = notlage(sinceLastSeek: .seconds(3))
#expect(DowngradeRule.verdict(facts) == .tooEarly)
}
@Test("Ein länger zurückliegender Sprung stört nicht")
func oldSeekIsFine() {
#expect(DowngradeRule.verdict(notlage(sinceLastSeek: .seconds(30))) == .downgrade)
}
@Test("Innerhalb der Sperrfrist nicht noch einmal")
func respectsCooldown() {
let facts = notlage(sinceLastDowngrade: .seconds(60), used: 1)
#expect(DowngradeRule.verdict(facts) == .cooling)
}
@Test("Nach der Sperrfrist wieder")
func afterCooldownAgain() {
let facts = notlage(sinceLastDowngrade: .seconds(200), used: 1)
#expect(DowngradeRule.verdict(facts) == .downgrade)
}
/// Lautlos die ganze Leiter bis 240p hinunterzugehen ist schlimmer, als
/// es zu sagen.
@Test("Das Budget ist endlich")
func budgetIsFinite() {
let facts = notlage(sinceLastDowngrade: .seconds(300), used: 2, budget: 2)
#expect(DowngradeRule.verdict(facts) == .budgetSpent)
}
/// Bei Live-TV nur einmal: jeder Neuaufbau stimmt den Tuner neu ab, und
/// davon gibt es vier Stück.
@Test("Bei Live-TV ist nach einem Mal Schluss")
func liveBudgetIsOne() {
let facts = notlage(sinceLastDowngrade: .seconds(300), used: 1, budget: 1)
#expect(DowngradeRule.verdict(facts) == .budgetSpent)
}
}