Files
Flaro/Packages/FlaroKit/Tests/FlaroPlaybackTests/StreamHealthTests.swift
T
ScarriffleandClaude Opus 5 a7cf6c5def Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine
eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt
so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner
Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging
daneben.

**Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den
deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche
Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in
Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store
der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden
frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler.

Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen
ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim
letzten Mal stattfand.

---

2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der
Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`,
`Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen
`FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und
das Image.

**Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen
Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes
Ersetzen hätte den sonst zerlegt.

`docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte
Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das
Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden
in die Irre führt.

**Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag:
`com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat
jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe.

308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS
bauen, das Image baut, und ein frischer Container läuft durch Einrichtung
und Anmeldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:38:27 +02:00

141 lines
4.7 KiB
Swift

import Testing
import Foundation
@testable import FlaroPlayback
@Suite("Zustand des Streams")
struct StreamHealthTests {
/// Baut eine Folge von Proben im Sekundentakt.
private func lauf(
_ health: inout StreamHealth,
von: Int,
bis: Int,
puffer: (Int) -> Double?,
stalls: (Int) -> Int = { _ in 0 },
keepUp: (Int) -> Bool? = { _ in true }
) -> StreamCondition {
var urteil = StreamCondition.healthy
for sekunde in von...bis {
urteil = health.record(StreamSample(
at: .seconds(sekunde),
bufferedAhead: puffer(sekunde).map { .milliseconds(Int($0 * 1000)) },
isLikelyToKeepUp: keepUp(sekunde),
stallCount: stalls(sekunde)
))
}
return urteil
}
@Test("Ein voller, stabiler Puffer ist gesund")
func fullBufferIsHealthy() {
var health = StreamHealth()
let urteil = lauf(&health, von: 0, bis: 60, puffer: { _ in 30 })
#expect(urteil == .healthy)
}
/// Der Fall, für den das Ganze gebaut ist: der Puffer schrumpft stetig.
/// Das muss auffallen, **bevor** er leer ist.
@Test("Ein schrumpfender Puffer meldet sich vor dem Rückler")
func shrinkingBufferWarnsEarly() {
var health = StreamHealth()
// Start mit 30 s, dann jede Sekunde eine weniger.
_ = lauf(&health, von: 0, bis: 10, puffer: { _ in 30 })
let urteil = lauf(&health, von: 11, bis: 36, puffer: { 30 - Double($0 - 10) })
#expect(urteil == .strained)
// Und zwar bei noch fünf Sekunden Puffer — nicht bei null.
#expect(health.condition != .failing)
}
@Test("Ein kleiner, aber stabiler Puffer ist kein Alarm")
func smallStableBufferIsNotAlarming() {
var health = StreamHealth()
let urteil = lauf(&health, von: 0, bis: 30, puffer: { _ in 4 })
// Klein, aber er hält. Das ist kein Grund, die Stufe zu senken.
#expect(urteil != .strained)
#expect(urteil != .failing)
}
@Test("Zwei Rückler in einer Minute heißen: trägt nicht")
func twoStallsMeanFailing() {
var health = StreamHealth()
let urteil = lauf(
&health,
von: 0,
bis: 40,
puffer: { _ in 10 },
stalls: { $0 < 10 ? 0 : ($0 < 25 ? 1 : 2) }
)
#expect(urteil == .failing)
}
@Test("Ein leerer Puffer heißt sofort: trägt nicht")
func emptyBufferIsFailing() {
var health = StreamHealth()
let urteil = lauf(&health, von: 0, bis: 20, puffer: { $0 < 18 ? 10 : 0 })
#expect(urteil == .failing)
}
/// „Noch nichts geladen" ist etwas anderes als „Puffer leer". Beim Start
/// darf das nicht als Notlage durchgehen.
@Test("Ohne Messwert kein Alarm")
func unknownBufferIsNotAlarming() {
var health = StreamHealth()
let urteil = lauf(&health, von: 0, bis: 10, puffer: { _ in nil })
#expect(urteil == .healthy)
}
/// Der Weg zurück ist absichtlich schmal: schnell runter, langsam rauf.
@Test("Zurück nach gesund braucht eine halbe Minute Ruhe")
func recoveryIsSlow() {
var health = StreamHealth()
_ = lauf(&health, von: 0, bis: 20, puffer: { $0 < 18 ? 10 : 0 })
#expect(health.condition == .failing)
// Puffer sofort wieder voll — aber zu kurz.
let früh = lauf(&health, von: 21, bis: 40, puffer: { _ in 30 })
#expect(früh != .healthy)
// Nach einer halben Minute durchgehend gut: erholt.
let spät = lauf(&health, von: 41, bis: 90, puffer: { _ in 30 })
#expect(spät == .healthy)
}
/// Ein einzelner Ausschlag von `isLikelyToKeepUp` kommt bei jedem Sprung
/// vor und darf nichts auslösen.
@Test("Ein einzelner Ausschlag löst nichts aus")
func singleKeepUpDipIsIgnored() {
var health = StreamHealth()
let urteil = lauf(
&health,
von: 0,
bis: 30,
puffer: { _ in 25 },
keepUp: { $0 == 15 ? false : true }
)
#expect(urteil == .healthy)
}
@Test("Anhaltende Sorge des Systems zählt")
func sustainedKeepUpFalseCounts() {
var health = StreamHealth()
let urteil = lauf(
&health,
von: 0,
bis: 30,
puffer: { _ in 25 },
keepUp: { $0 >= 27 ? false : true }
)
#expect(urteil == .strained)
}
@Test("Zurücksetzen macht einen frischen Anfang")
func resetStartsOver() {
var health = StreamHealth()
_ = lauf(&health, von: 0, bis: 20, puffer: { $0 < 18 ? 10 : 0 })
#expect(health.condition == .failing)
health.removeAll()
#expect(health.condition == .healthy)
}
}