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>
141 lines
4.7 KiB
Swift
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)
|
|
}
|
|
}
|