diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ec921a5..5b2a3a6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,10 +6,22 @@ # sans cette entrée, l'épinglage devient une dette que personne ne relit. Les deux vont # ensemble ou aucun des deux ne tient. # -# La cadence est MENSUELLE et les mises à jour sont GROUPÉES en une seule demande par -# écosystème. Une coopérative n'a pas de mainteneur à temps plein : une demande par -# dépendance et par semaine, c'est un dépôt où l'on cesse de regarder les demandes — donc -# un dépôt où la mise à jour de sécurité passe inaperçue avec les autres. +# La cadence est MENSUELLE et les mises à jour sont GROUPÉES. Une coopérative n'a pas de +# mainteneur à temps plein : une demande par dépendance et par semaine, c'est un dépôt où +# l'on cesse de regarder les demandes — donc un dépôt où la mise à jour de sécurité passe +# inaperçue avec les autres. +# +# MAIS LES MAJEURES VOYAGENT SÉPARÉMENT DES MINEURES, et cette ligne est écrite après un +# échec. La première version de ce fichier groupait tout en `patterns: ["*"]` par +# écosystème. La demande npm qui en est sortie portait six montées, dont TypeScript 7 — +# que `svelte-check` refuse encore (`peer typescript@"^5.0.0 || ^6.0.0"`). Résultat : +# `npm ci` échouait AVANT le premier test, et cinq montées parfaitement saines étaient +# retenues par la sixième, sans autre issue que de fermer la demande entière. +# +# Deux groupes par écosystème coûtent une demande de plus et rendent les mineures +# indépendantes : elles passent seules, et une majeure qui casse peut rester en attente +# aussi longtemps qu'il faut sans rien bloquer. C'est la seule chose qui distingue une +# montée qu'on relit d'une montée qu'on subit. version: 2 updates: # --- Go --------------------------------------------------------------------------- @@ -24,6 +36,10 @@ updates: groups: go: patterns: ["*"] + update-types: ["minor", "patch"] + go-majeur: + patterns: ["*"] + update-types: ["major"] commit-message: prefix: build(deps) @@ -37,6 +53,10 @@ updates: groups: actions: patterns: ["*"] + update-types: ["minor", "patch"] + actions-majeur: + patterns: ["*"] + update-types: ["major"] commit-message: prefix: ci(deps) @@ -52,6 +72,12 @@ updates: # # Tout est en devDependencies : rien de ce bloc ne part sur un poste, seul le bundle # construit voyage. + # + # C'est ICI que le groupement unique a fait le plus de dégâts, et c'est pourquoi la + # séparation compte le plus : les dépendances du front sont épinglées à la version + # EXACTE (§14.1), donc chacune de leurs majeures est une rupture potentielle de peer + # dependency. TypeScript 7 en est une, et elle attendra dans `front-majeur` que + # `svelte-check` l'accepte, sans retenir les correctifs des cinq autres. - package-ecosystem: npm directory: /web schedule: @@ -61,5 +87,9 @@ updates: groups: front: patterns: ["*"] + update-types: ["minor", "patch"] + front-majeur: + patterns: ["*"] + update-types: ["major"] commit-message: prefix: build(front) diff --git a/SUIVI.md b/SUIVI.md index 2fdf569..924e12d 100644 --- a/SUIVI.md +++ b/SUIVI.md @@ -68,12 +68,50 @@ l'écrire. Un fait qui les renforce, vérifié plutôt que supposé : la table ` porte **aucun identifiant de client** — donc son export ouvert n'expose pas de donnée personnelle. -**Une observation restée ouverte.** Sur douze exécutions de la suite complète, **une** a -signalé un échec dans un paquet, jamais reproduit ensuite — et la commande de comptage qui -l'a vu n'en a pas gardé le nom. Ce n'est pas imputable aux ajouts ci-dessus : les trois -tests nouveaux ont tenu 50 exécutions chacun, et `internal/web` dix passes sous `-race`. -`ci.yml` documente déjà une famille de tests sensibles au planificateur (`skipUnderShort`), -et c'est la première piste si le symptôme revient. +**L'échec intermittent a un nom, et la première piste était fausse (30/07/2026).** L'entrée +ci-dessus le laissait ouvert : une exécution de la suite sur douze signalait un échec dans un +paquet, jamais reproduit, et la commande de comptage n'en avait pas gardé le nom. Elle +renvoyait vers la famille de tests sensibles au planificateur que `ci.yml` documente +(`skipUnderShort`). **Ce n'était pas ça.** + +La CI l'a rattrapé sur la demande Dependabot des actions — une demande qui ne change que +deux empreintes dans deux fichiers YAML, donc dont l'échec ne pouvait pas venir du code +testé : + +``` +--- FAIL: TestAnAmputatedCatalogIsRefusedAgainstTheRealGuard (5.54s) + failures_catalog_test.go:1619: la grille n'a jamais compté 331 tuiles +``` + +Ce n'est pas une course entre goroutines, c'est un **budget d'attente sans marge**. Mesuré +sur ce test — le plus lourd du paquet, il décode les 181 images du `flv.csv` réel deux fois : + +| Condition | Durée | +|---|---| +| isolé, sans `-race` | 0,52 s (cinq exécutions, stable) | +| isolé, sous `-race` | 1,07 s | +| budget `hang` | 5 s | +| observé en CI à l'échec | 5,54 s | + +L'écart vient du runner. `go test ./...` joue les paquets **en parallèle**, donc +`internal/web` et `cmd/openscale` — 25 s et 21 s à eux seuls — concourent avec celui-ci sur +quatre vCPU, sous le détecteur de courses. Une seconde de travail en devient cinq, et un +garde-fou qui n'avait qu'un facteur cinq de marge n'en a plus. + +`hang` passe donc de 5 s à **30 s**, avec les mesures écrites à côté de la constante. Le +changement ne coûte **rien** sur une exécution verte — l'attente rend la main dès que sa +condition tient, aucun test qui passe ne ralentit d'une microseconde. Il coûte trente +secondes au lieu de cinq pour déclarer un vrai blocage, une fois, sur une exécution qui +échoue déjà. C'est l'arbitrage que le commentaire de la constante revendiquait depuis le +début — « it is a guard, not a delay » — et que la valeur 5 ne tenait pas. + +**Et les deux groupes de `dependabot.yml`, pour la même raison qu'on écrit les échecs.** La +première version du fichier groupait tout par écosystème en `patterns: ["*"]`. La demande +npm qui en est sortie portait six montées dont TypeScript 7, que `svelte-check` refuse +encore (`peer typescript@"^5.0.0 || ^6.0.0"`) : `npm ci` échouait avant le premier test, et +cinq montées saines étaient retenues par la sixième sans autre issue que de fermer la +demande entière. Majeures et mineures voyagent désormais séparément, dans les trois +écosystèmes. **Il y a bien un gcc sur ce poste, et les relevés précédents disaient le contraire.** La passe `-race` exige cgo, donc un compilateur C, et trois entrées de suivi de suite l'ont diff --git a/internal/station/harness_test.go b/internal/station/harness_test.go index 8869010..3c1cdb2 100644 --- a/internal/station/harness_test.go +++ b/internal/station/harness_test.go @@ -33,7 +33,30 @@ const ( // hang is how long a test waits for something that should be immediate before it // declares a deadlock. It never elapses in a passing run: it is a guard, not a // delay, and no assertion depends on its value. -const hang = 5 * time.Second +// +// # Thirty seconds, and it used to be five +// +// Five was not a margin, it was a coincidence that held on a development machine. CI +// caught TestAnAmputatedCatalogIsRefusedAgainstTheRealGuard at 5.54 s on 30/07/2026, on a +// pull request that changed nothing but two action SHAs in YAML — so the failure could not +// have come from the code under test. MEASURED afterwards, on the same test, which is the +// heaviest of this package because it decodes the 181 images of the REAL flv.csv twice: +// +// isolated, no -race 0.52 s (five runs, stable) +// isolated, -race 1.07 s +// budget 5 s +// observed in CI 5.54 s +// +// The gap is the runner. `go test ./...` runs the packages CONCURRENTLY, so internal/web +// and cmd/openscale — 25 s and 21 s of their own — compete with this one on four vCPUs, +// under the race detector. One second of work becomes five, and a guard with a factor of +// five of headroom has none left. +// +// Raising it costs NOTHING on a green run: the wait returns as soon as its condition +// holds, so no passing test gets slower by a microsecond. What it costs is thirty seconds +// instead of five to declare a genuine deadlock, once, on a run that is already failing. +// That is the trade the sentence above claims to make, and five did not honour it. +const hang = 30 * time.Second // loadConfig reads the configuration actually shipped with the binary. //