Les widgets reçoivent la bande et les préréglages au lieu de les inventer
La couleur du score de forme était décidée ici, avec un barème périmé (vert >= 75, jaune >= 50). Le serveur avait recalé le sien sur la distribution réelle (65/55/40) parce que le score est borné à [25, 75] : une bande haute à 75 est inatteignable. Mesuré sur 115 jours de production, le vert n'est jamais sorti et le rouge couvrait 68 jours. La correction n'avait pas atteint le binaire. `FormeScore` porte désormais la bande servie par le serveur, et `resolvedBand` ne sert que de repli — aligné sur les seuils serveur, pas sur les anciens. Le widget de saisie rapide enregistrait un café à 100 ml, volume qui ne correspond à aucun préréglage : le serveur avait séparé l'expresso (60) du mug (250) parce qu'un volume unique faussait le journal, et ces préréglages sont personnalisables. Les boutons viennent maintenant du snapshot ; `fallbackQuickDrinks` reprend les préréglages par défaut du serveur. 15 tests ajoutés au banc d'essai Linux, qui compile réellement ces deux fichiers : 25 tests verts. Le reste (SwiftUI, WidgetKit) n'est pas compilable ici et n'a été que relu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
77
tests-linux/Tests/CoachModelTests/FormeBandTests.swift
Normal file
77
tests-linux/Tests/CoachModelTests/FormeBandTests.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user