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,143 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import HaloPlayback
|
||||
|
||||
@Suite("Wann hinaufgeschaltet wird")
|
||||
struct UpgradeRuleTests {
|
||||
|
||||
/// Die Lage, in der es erlaubt ist — alle übrigen Tests weichen davon in
|
||||
/// genau einem Punkt ab. So steht in jedem Test nur der Unterschied.
|
||||
private func ruhig(
|
||||
isAutomatic: Bool = true,
|
||||
condition: StreamCondition = .healthy,
|
||||
hasHigherRung: Bool = false,
|
||||
ceilingAllowsMore: Bool = true,
|
||||
bufferedAhead: Duration? = .seconds(30),
|
||||
playedFor: Duration = .seconds(120),
|
||||
sinceLastStall: Duration? = nil,
|
||||
sinceLastSeek: Duration? = nil,
|
||||
sinceLastUpgrade: Duration? = nil,
|
||||
used: Int = 0,
|
||||
budget: Int = 2
|
||||
) -> UpgradeRule.Facts {
|
||||
.init(
|
||||
isAutomatic: isAutomatic,
|
||||
condition: condition,
|
||||
hasHigherRung: hasHigherRung,
|
||||
ceilingAllowsMore: ceilingAllowsMore,
|
||||
bufferedAhead: bufferedAhead,
|
||||
playedFor: playedFor,
|
||||
sinceLastStall: sinceLastStall,
|
||||
sinceLastSeek: sinceLastSeek,
|
||||
sinceLastUpgrade: sinceLastUpgrade,
|
||||
used: used,
|
||||
budget: budget
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Läuft es ruhig und ist die Leiter ausgereizt, wird neu aufgebaut")
|
||||
func calmAndCappedRebuilds() {
|
||||
#expect(UpgradeRule.verdict(ruhig()) == .rebuildHigher)
|
||||
}
|
||||
|
||||
// MARK: - Was es verbietet
|
||||
|
||||
/// Wer „720p · 3 Mbps" antippt, meint das — auch wenn die Leitung mehr
|
||||
/// trüge.
|
||||
@Test("Eine gewählte Stufe wird nicht überstimmt")
|
||||
func neverOverridesAChoice() {
|
||||
#expect(UpgradeRule.verdict(ruhig(isAutomatic: false)) == .notAutomatic)
|
||||
}
|
||||
|
||||
/// **Der wichtigste Fall.** Solange die Leiter noch eine Sprosse hat,
|
||||
/// kostet das Hinaufgehen kein schwarzes Bild — ein Neuaufbau dafür wäre
|
||||
/// Verschwendung.
|
||||
@Test("Solange die Leiter trägt, kein Neuaufbau")
|
||||
func prefersTheCheapPath() {
|
||||
#expect(UpgradeRule.verdict(ruhig(hasHigherRung: true)) == .cheaperPathRemains)
|
||||
}
|
||||
|
||||
@Test("Über die eingestellte Grenze geht es nicht")
|
||||
func respectsTheSetting() {
|
||||
#expect(UpgradeRule.verdict(ruhig(ceilingAllowsMore: false)) == .atCeiling)
|
||||
}
|
||||
|
||||
@Test("Bei angespannter Lage wird nicht hinaufgegangen")
|
||||
func notWhenStrained() {
|
||||
#expect(UpgradeRule.verdict(ruhig(condition: .strained)) == .notCalmEnough)
|
||||
#expect(UpgradeRule.verdict(ruhig(condition: .failing)) == .notCalmEnough)
|
||||
}
|
||||
|
||||
/// „Es läuft gerade" genügt nicht — es muss etwas auf der hohen Kante
|
||||
/// liegen, sonst trägt es den Neuaufbau nicht.
|
||||
@Test("Ohne Vorrat kein Neuaufbau")
|
||||
func needsABuffer() {
|
||||
#expect(UpgradeRule.verdict(ruhig(bufferedAhead: .seconds(5))) == .notCalmEnough)
|
||||
#expect(UpgradeRule.verdict(ruhig(bufferedAhead: nil)) == .notCalmEnough)
|
||||
}
|
||||
|
||||
/// Der Puffer füllt sich nach dem Start immer, und das sieht eine halbe
|
||||
/// Minute lang nach einer hervorragenden Leitung aus.
|
||||
@Test("Am Anfang nicht, auch wenn alles gut aussieht")
|
||||
func notRightAfterStart() {
|
||||
#expect(UpgradeRule.verdict(ruhig(playedFor: .seconds(20))) == .tooEarly)
|
||||
}
|
||||
|
||||
@Test("Nach einem Sprung nicht")
|
||||
func notRightAfterSeek() {
|
||||
#expect(UpgradeRule.verdict(ruhig(sinceLastSeek: .seconds(3))) == .tooEarly)
|
||||
}
|
||||
|
||||
/// Wer gerade gestockt hat, hat keine Reserve — und ein Neuaufbau wäre
|
||||
/// die zweite Unterbrechung binnen einer Minute.
|
||||
@Test("Nach einem Rückler lange nicht")
|
||||
func waitsAfterAStall() {
|
||||
#expect(UpgradeRule.verdict(ruhig(sinceLastStall: .seconds(30))) == .tooEarly)
|
||||
#expect(UpgradeRule.verdict(ruhig(sinceLastStall: .seconds(120))) == .rebuildHigher)
|
||||
}
|
||||
|
||||
@Test("Die Sperrfrist gilt")
|
||||
func respectsTheCooldown() {
|
||||
#expect(UpgradeRule.verdict(ruhig(sinceLastUpgrade: .seconds(60))) == .cooling)
|
||||
#expect(UpgradeRule.verdict(ruhig(sinceLastUpgrade: .seconds(200))) == .rebuildHigher)
|
||||
}
|
||||
|
||||
@Test("Das Budget ist endlich")
|
||||
func budgetIsFinite() {
|
||||
#expect(UpgradeRule.verdict(ruhig(used: 2, budget: 2)) == .budgetSpent)
|
||||
#expect(UpgradeRule.verdict(ruhig(used: 1, budget: 2)) == .rebuildHigher)
|
||||
}
|
||||
|
||||
// MARK: - Die Reihenfolge der Gründe
|
||||
|
||||
/// Bei mehreren Gründen soll der **grundsätzlichste** gemeldet werden —
|
||||
/// sonst steht im Protokoll „Sperrfrist", wo in Wahrheit die Einstellung
|
||||
/// das Ende ist, und jemand wartet auf etwas, das nie kommt.
|
||||
@Test("Der grundsätzlichste Grund gewinnt")
|
||||
func reportsTheMostFundamentalReason() {
|
||||
#expect(
|
||||
UpgradeRule.verdict(ruhig(
|
||||
isAutomatic: false, hasHigherRung: true, ceilingAllowsMore: false
|
||||
)) == .notAutomatic
|
||||
)
|
||||
#expect(
|
||||
UpgradeRule.verdict(ruhig(
|
||||
hasHigherRung: true, ceilingAllowsMore: false
|
||||
)) == .atCeiling
|
||||
)
|
||||
#expect(
|
||||
UpgradeRule.verdict(ruhig(
|
||||
condition: .failing, hasHigherRung: true
|
||||
)) == .cheaperPathRemains
|
||||
)
|
||||
}
|
||||
|
||||
/// Das Hinaufschalten muss zurückhaltender sein als das Absenken: ein zu
|
||||
/// spätes Absenken bedeutet Ruckeln, ein zu spätes Hinaufschalten ein
|
||||
/// etwas weicheres Bild.
|
||||
@Test("Zurückhaltender als das Absenken")
|
||||
func moreCautiousThanDowngrading() {
|
||||
#expect(UpgradeRule.settleIn > DowngradeRule.settleIn)
|
||||
#expect(UpgradeRule.cooldown < DowngradeRule.cooldown)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user