Skip to content

ci(e2e): un vert qui veut dire quelque chose — les parcours connectés tournent enfin - #271

Merged
thierryvm merged 6 commits into
mainfrom
ci/refonte-02-filet-e2e
Jul 26, 2026
Merged

ci(e2e): un vert qui veut dire quelque chose — les parcours connectés tournent enfin#271
thierryvm merged 6 commits into
mainfrom
ci/refonte-02-filet-e2e

Conversation

@thierryvm

Copy link
Copy Markdown
Owner

Étape 2 / 17. Rendre un gh pr checks ✅ significatif. Aujourd'hui il ne l'est pas.

Le problème, chiffré

Run 30210147687 sur main : 214 cas passent, 173 sautent — 44,7 % de la suite. Les 173 contiennent tous les parcours connectés. Le vert ne couvre que la surface publique.

Six blocages, tous vérifiés en code

# Blocage Pourquoi c'est fatal
B1 adminClientOrNull() sautait sur localhost:54321 C'est l'adresse exacte que sert supabase start. Démarrer une stack locale n'aurait rien changé.
B2 rateLimit() échoue fermé, next start force NODE_ENV=production Pas seulement le login : les 24 sites rateLimit('mutation') aussi.
B3 Quota auth = 5 / 15 min par IP 39 sites goto('/login') × 3 projets → 100+ connexions par passe.
B4 Pas de supabase/config.toml supabase start en exige un.
B5 Le détecteur de specs ratait 3 fichiers e2e:auth --all annonçait tout couvrir en exécutant 9 specs sur 13.
B6 GoTrue a ses propres quotas — 30 / 5 min par IP Aucune revue ne l'avait vu. Aurait fait échouer la suite en simulant un login cassé.

Ce que la revue a empêché

Ma v1 proposait un rate-limiter « en mémoire » pour les tests, désactivé si NEXT_PUBLIC_APP_ENV === 'production'. Fail-open : cette variable a un .default('development'). Oubliée ou non propagée au scope Preview → le contournement s'active. Or les previews pointent sur l'unique projet Supabase, la production.

Le plan final ne touche aucune ligne de src/. Une IP unique par test, et un Redis réel dans le job — la suite gagne la couverture du rate-limiting au lieu de la perdre.

Mesuré avant d'être poussé

Cas exécutés 24 passed, 0 failed
Durée 1,0 min
Connexions réelles servies 79
Refus de rate-limit 0

Première passe : 29 échecs en 24,8 min. Cause : Playwright exécute chaque réessai dans un worker neuf, qui remettait mon compteur d'IP à zéro — chaque réessai réutilisait l'adresse du premier test, épuisait le budget, et transformait quelques échecs en cascade. Dérivée de testId + retry : 29 → 12 → 0.

Acquis permanent : les migrations rejouent depuis zéro

15 migrations → 14 tables sur une base vide. Jamais vérifié auparavant : la production ne les a reçues qu'incrémentalement. Fidélité confirmée, Postgres 17.6 des deux côtés.

Ce que ça révèle, et que je ne corrige pas ici

Six specs décrivent un tableau de bord qui n'existe plus. Mergées, puis le cockpit a été reconstruit, et rien n'a protesté puisqu'elles ne tournaient nulle part. Trois cherchent des data-testid présents dans zéro fichier de src/. Trois cherchent un rôle heading sur CardTitle, qui rend une <div>.

Quarantaine explicite : découvertes, listées, comptées, imprimées à chaque run avec leur raison. Aucun test.skip ajouté.

Trois dettes tracées : un défaut d'a11y réel (les titres du cockpit ne sont pas des titres), l'audit RGPD qui ne s'écrit pas sur une base reconstruite depuis les migrations (donc un droit posé hors migration en prod), et un angle mort du préflight sur le compte Supabase actif.

Incident de comptes évité

supabase start a échoué : le projet professionnel OVB occupe les ports par défaut sur la machine, et le CLI suggérait d'arrêter sa stack. Ports d'Ankora décalés — les deux coexistent, aucune commande d'ici ne peut plus toucher au projet pro.

⚠️ L'étape n'est livrée qu'à moitié à la fusion

La protection de branche est hors périmètre (PR dédiée, revue humaine). Le job ne sera pas dans les checks requis : il pourra être rouge sans bloquer un merge. Tant qu'une PR de suivi ne l'ajoute pas, le filet reste décoratif.

Rapport complet : docs/prs/PR-refonte-02-filet-e2e-report.md

thierryvm and others added 5 commits July 26, 2026 19:17
Three guards stood between CI and the authenticated journeys, and none of
them announced itself.

`adminClientOrNull()` decided whether Supabase was real by checking whether
the URL contained `localhost:54321` — the exact address `supabase start`
serves. Standing up a local stack in CI would therefore have changed nothing:
all 173 skipped cases would have kept skipping. Readiness is now declared with
E2E_SUPABASE_READY, and a declared-but-broken environment throws instead of
skipping, because a green mostly-skipped run is the failure being fixed.

`rateLimit('auth', …)` allows 5 attempts per 15 minutes keyed by IP, and the
suite performs well over a hundred real logins. Each test now carries its own
`x-forwarded-for`, which the app already trusts as the caller's address
(Vercel overwrites it upstream), so the buckets isolate without a single line
of production code changing.

GoTrue enforces its own quota on top of ours — `sign_in_sign_ups` defaults to
30 per 5 minutes per IP. Left alone it would have failed the suite while
looking like a broken login. `supabase/config.toml` raises it for the
ephemeral stack only; production is configured in the dashboard.

The config is hand-trimmed from the 396 lines `supabase init` emits: realtime,
storage, studio, analytics, edge_runtime and the pooler are off (measured
unused), inbucket stays on because the forgot-password spec needs a mail sink.
Ports are shifted off the Supabase defaults — the professional project runs its
own stack on this machine, and `supabase start` suggested stopping *that* one.

Both unconditional skips are gone. The anti-enumeration spec only ever needed a
reachable Supabase. The locale soft-navigation spec was skipped on a false
premise: it hunted a FAQ link the desktop landing does not render, but the
landing does mount MktFooter with real next-intl Links — it needed any
prefetched Link, not that one. Selector now matches the href, since the label
is precisely what the locale switch changes.

Spec selection is shared between the local runner and CI, and the expected list
is committed. Discovery previously matched `adminClientOrNull` alone and missed
every spec seeding through the `seededUser` fixture, so `e2e:auth --all` had
been running 9 specs while reporting it ran them all. Divergence in either
direction now fails loudly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A second Playwright job boots an ephemeral Supabase stack, applies the 15
migrations to an empty database, and runs the specs that seed a user and log
in. Measured locally before being pushed: 24 cases, 0 failures, 1 minute,
79 real logins served with zero rate-limit refusals.

It runs OUTSIDE a job container, unlike `e2e`. `supabase start` needs the
Docker daemon, and from inside a container the Supabase containers would be
siblings on the host with localhost routing nowhere. Two address spaces
coexist in the job and confusing them is the likeliest way to break it: SRH
publishes 8079 for the host-side Next server, and reaches Redis by service
name on the job network.

The per-test IP is derived from the test's identity, not a counter. A counter
looked equivalent and was not: Playwright runs retries in a FRESH worker,
which re-imports the module and resets it to zero, so every retry reused the
first test's address. A handful of failures piled their retries onto the same
few IPs, exhausted the 5-per-15-minutes budget and turned isolated failures
into a cascade of "Trop de tentatives" — 29 failures in 24.8 minutes, against
12 in 5.4 once derived from testId + retry.

Six specs are quarantined, listed with a reason each and printed on every run.
They describe a dashboard that no longer exists: three assert data-testids
present in ZERO files under src/ (effort-financier-card, substat-*,
capacite-epargne-*) — the hero radar the Situation Hero replaced — and three
look for a heading role on CardTitle, which renders a div. They were merged,
the cockpit was rebuilt, and nothing objected because they had never executed
anywhere. Running them for the first time is what surfaced it.

That last cause is also a real accessibility defect: the cockpit's section
titles are not headings for a screen reader. Left for its own PR, since this
one touches no src/.

Also surfaced and left alone: audit logging fails outright on a database
rebuilt from the migrations (permission denied for table audit_log). It works
in production, so a grant was applied there outside the migration set — the
migrations are not a complete description of production, which matters for a
GDPR-mandated audit trail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six are quarantined; the table said otherwise two sections above the section
that explains the quarantine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the permanent DoD criterion: the number of e2e cases executed never
decreases. Two jobs, so two floors — a single aggregate number would be
unreadable at the first conflict, and an unmeasurable criterion is an ignored
one. Floors as measured on 2026-07-26: 214 for the public job, 24 for the
authenticated one.

Also states that the quarantine list may only shrink. Adding to it is an
admission that gets justified in writing, not a shortcut to a green pipeline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ankora Ready Ready Preview, Comment Jul 26, 2026 6:42pm

@github-actions github-actions Bot added area:ci CI / GitHub Actions / workflows status:review-needed Ready for review labels Jul 26, 2026
…iling

The unconditional skip was lifted and the spec added to the authenticated
job's selection — but nothing stopped it also running in the PUBLIC job,
which has no Supabase. It failed there three times, once per browser
project, exactly the contradiction the plan review flagged and that I only
half-closed.

The skip is now CONDITIONAL on E2E_SUPABASE_READY, the same declared-readiness
signal the seed helper uses: it runs in the authenticated job and reports
itself skipped elsewhere, with the reason attached rather than vanishing.
`grep "test.skip('"` still returns zero — the ban was on unconditional skips,
not on stating a real precondition.

Public job floor raised 214 → 215: the locale soft-navigation spec, skipped
since May on a mistaken premise, passes there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thierryvm
thierryvm merged commit d652d1c into main Jul 26, 2026
9 checks passed
@thierryvm
thierryvm deleted the ci/refonte-02-filet-e2e branch July 26, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci CI / GitHub Actions / workflows status:review-needed Ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant