Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit, damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind. 712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25 Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit `HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`, `JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben. `docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben hiesse, die Vergangenheit zu glätten. **Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis: `com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container; Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen. **Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter `…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet. Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht statt umbenannt — sie steht ohnehin nicht im Repo. Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist ein Handgriff ausserhalb des Projekts. 368 Tests grün. iOS und tvOS bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2fcf6821c8
commit
4b9a1e7b67
@@ -0,0 +1,128 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import HaloPlayback
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user