Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------------------------------
Expand All @@ -24,6 +36,10 @@ updates:
groups:
go:
patterns: ["*"]
update-types: ["minor", "patch"]
go-majeur:
patterns: ["*"]
update-types: ["major"]
commit-message:
prefix: build(deps)

Expand All @@ -37,6 +53,10 @@ updates:
groups:
actions:
patterns: ["*"]
update-types: ["minor", "patch"]
actions-majeur:
patterns: ["*"]
update-types: ["major"]
commit-message:
prefix: ci(deps)

Expand All @@ -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:
Expand All @@ -61,5 +87,9 @@ updates:
groups:
front:
patterns: ["*"]
update-types: ["minor", "patch"]
front-majeur:
patterns: ["*"]
update-types: ["major"]
commit-message:
prefix: build(front)
50 changes: 44 additions & 6 deletions SUIVI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion internal/station/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down