// La bande du score de forme vient du serveur, plus de seuils recopiés. // // ⚠️ **Ce qui est en jeu.** La complication watchOS et les widgets d'accueil // coloraient le score avec leurs propres seuils : vert ≥ 75, jaune 50-74, rouge // en dessous. Le serveur avait pourtant recalé les siens (65/55/40) en // documentant pourquoi : le score est borné à [25, 75] par construction, donc // une bande haute à 75 est inatteignable. // // Mesuré sur 115 jours de production : le vert n'est jamais sorti (0 jour), et // le rouge couvrait 68 jours sur 115. La correction serveur n'avait pas atteint // le binaire, qui ne se déploie qu'au build suivant. // // Le natif ne décide donc plus : il reçoit `band` et se contente de la traduire // en couleur. Le repli, pour un snapshot écrit par une version antérieure, // reprend les seuils du serveur — jamais les anciens. import XCTest @testable import CoachModel final class FormeBandTests: XCTestCase { private func snapshot(score: Int?, band: String?) -> CoachWidgetSnapshot { var s = CoachWidgetSnapshot(updatedAt: Date(timeIntervalSince1970: 1_755_000_000)) s.forme = CoachWidgetSnapshot.FormeScore(score: score, label: nil, band: band) return s } func testTheBandFromTheServerIsUsedAsIs() { let s = snapshot(score: 60, band: "good") XCTAssertEqual(s.forme?.resolvedBand, .good) } func testTheCaseThatWasWrong() { // 60 : « bonne récupération » côté serveur, rouge sur la montre. let s = snapshot(score: 60, band: "good") XCTAssertEqual(s.forme?.resolvedBand, .good) XCTAssertNotEqual(s.forme?.resolvedBand, .low) } func testAnUnknownBandFallsBackOnTheScore() { XCTAssertEqual(snapshot(score: 60, band: "farfelu").forme?.resolvedBand, .good) } func testWithoutABandTheServerThresholdsApply() { // Repli aligné sur RECOVERY_BANDS : 65 / 55 / 40. XCTAssertEqual(snapshot(score: 69, band: nil).forme?.resolvedBand, .great) XCTAssertEqual(snapshot(score: 65, band: nil).forme?.resolvedBand, .great) XCTAssertEqual(snapshot(score: 64, band: nil).forme?.resolvedBand, .good) XCTAssertEqual(snapshot(score: 55, band: nil).forme?.resolvedBand, .good) XCTAssertEqual(snapshot(score: 54, band: nil).forme?.resolvedBand, .medium) XCTAssertEqual(snapshot(score: 40, band: nil).forme?.resolvedBand, .medium) XCTAssertEqual(snapshot(score: 39, band: nil).forme?.resolvedBand, .low) } func testTheObservedRangeIsCoveredByMoreThanOneBand() { // Sur 115 jours, les scores vont de 30 à 69. Un barème qui rendrait la // même bande sur toute cette plage ne dirait plus rien. let bands = Set([30, 37, 45, 60, 69].map { snapshot(score: $0, band: nil).forme?.resolvedBand }) XCTAssertGreaterThan(bands.count, 1) XCTAssertTrue(bands.contains(.great), "le haut de la plage doit être atteignable") } func testNoScoreNoBand() { XCTAssertNil(snapshot(score: nil, band: nil).forme?.resolvedBand) } func testAnOlderSnapshotStillDecodes() throws { // Un snapshot écrit avant l'ajout du champ `band` doit rester lisible. let json = """ {"updatedAt": 1755000000, "forme": {"score": 58, "label": "Modéré"}} """.data(using: .utf8)! let decoded = try JSONDecoder().decode(CoachWidgetSnapshot.self, from: json) XCTAssertEqual(decoded.forme?.score, 58) XCTAssertEqual(decoded.forme?.resolvedBand, .good) } }