Files
coach-ios/tests-linux/Tests/CoachModelTests/FormeBandTests.swift
Sylvain Bettinelli 4cff1d4603 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>
2026-08-19 11:42:19 +00:00

78 lines
3.5 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
}