Files
MaksTinyWorkshop f1b783407a docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md
(lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation
remote antérieure (triage 2026-05-02).

~73 entrées intégrées sur knowledge/backend/, dont :
- patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist
- patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C),
  row réactivable, endpoint replace atomique, updateMany conditionnel, etc.
- risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste,
  fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.)
- patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests
- compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...)
- README patterns/risques mis à jour

Doublons internes corrigés en relecture (suppression-champ .map() → general seul ;
e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés.
Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:25:02 +02:00

24 KiB
Raw Permalink Blame History


title: Backend — Patterns : Tests domain: backend bucket: patterns tags: [tests, vitest, prisma, integration, isolation] applies_to: [implementation, review, debug] severity: high validated_on: 2026-05-02 source_projects: [RL799_V2]

Backend — Patterns : Tests

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/patterns/README.md pour l'index complet.


Pattern : cleanup.track() LIFO pour tests d'intégration DB

  • Objectif : nettoyer proprement les artefacts créés par chaque test sans recourir à un TRUNCATE CASCADE qui casserait les transactions du SUT.
  • Contexte : tests qui écrivent dans une vraie base (pas mockée), en cohabitation avec un seed durable et des tables FK-liées.
  • Quand l'utiliser : dès qu'un test crée des rows à la volée et qu'on veut éviter la pollution inter-tests.
  • Quand l'éviter : si le projet utilise transactions + rollback par test (déjà isolé), ou si le SUT n'écrit jamais en DB.
  • Avantage :
    • ordre LIFO = ordre FK-safe (les enfants tombent avant les parents)
    • résistant aux throws intermédiaires (le afterEach tourne quand même)
    • pas de TRUNCATE qui casserait les transactions ouvertes par le SUT
  • Limites / vigilance :
    • convention "100 % via cleanup.track" : ne pas mélanger avec des prisma.*.deleteMany directs
    • la queue est pour le teardown, pas pour le setup : ne jamais tracker une création
  • Validé le : 24-04-2026
  • Contexte technique : Vitest / Prisma / Postgres — RL799_V2

Implémentation (helper minimal)

// __tests__/helpers/db.ts
type CleanupFn = () => Promise<void> | void;

export function createCleanup() {
  const queue: CleanupFn[] = [];
  return {
    track(fn: CleanupFn) { queue.push(fn); },
    async run() {
      while (queue.length > 0) {
        const fn = queue.pop()!;
        try { await fn(); }
        catch (err) { console.warn('cleanup.run error:', err); }
      }
    },
  };
}

Usage typique

const cleanup = createCleanup();
afterEach(async () => { await cleanup.run(); });

test('crée une tenue', async () => {
  const tenue = await prisma.tenue.create({ data: { /* … */ } });
  cleanup.track(async () => {
    await prisma.tenue.deleteMany({ where: { id: tenue.id } });
  });
  // … assertions
});

Checklist

  • Ordre LIFO respecté (FK-safe)
  • Aucun prisma.*.deleteMany direct hors cleanup.track dans les fichiers concernés
  • Cleanup défensif (try/catch + warn) — pas de partial leak
  • Aucune création (seulement des suppressions/restores) dans la queue

Pattern : globalSetup vitest pour purger les résidus DB inter-runs

  • Objectif : repartir d'une DB propre en début de suite quand des cleanups intra-test sont incomplets et que les artefacts s'accumulent entre sessions.
  • Contexte : projet vitest avec maxWorkers: 1 (DB partagée), tables 100 % alimentées par les tests (notifications, audit logs, payments éphémères).
  • Quand l'utiliser : symptômes de "tests verts en isolation, rouges en suite complète après plusieurs jours" liés à des findFirst({ type }) qui tombent sur des résidus.
  • Quand l'éviter : si la suite tourne déjà avec une stratégie transactions + rollback ou DB-per-worker.
  • Avantage :
    • hook standard vitest, pas de cron externe ni de prisma:reset manuel entre sessions
    • 1 seule passe au démarrage de la suite, coût négligeable
  • Limites / vigilance :
    • NE PAS purger les tables seed (users, soirees seed, profiles, etc.) — réservé aux tables 100 % "test-only"
    • ne corrige pas la flakiness intra-run entre fichiers consécutifs
  • Validé le : 25-04-2026
  • Contexte technique : Vitest / Prisma — RL799_V2

Implémentation

// vitest.config.ts
test: {
  maxWorkers: 1,
  globalSetup: ['./src/__tests__/globalSetup.ts'],
}

// src/__tests__/globalSetup.ts
export default async function globalSetup() {
  await prisma.notification.deleteMany();
  await prisma.auditLog.deleteMany();
  await prisma.cotisationPayment.deleteMany();
  await prisma.$disconnect();
}

Vérification avant d'ajouter une table

# Tables référencées par un snapshot/seed → NE PAS purger
grep -rln "prisma.<entity>.findMany\|seedSnapshot" src/__tests__/

Checklist

  • Aucune table seed dans la liste des deleteMany
  • Hook référencé dans vitest.config.ts
  • Justifier en commentaire pourquoi chaque table listée est test-only

Pattern : Template database Postgres pour isoler les fichiers de tests

  • Objectif : éliminer la flakiness inter-fichiers en garantissant que chaque fichier de tests démarre avec une DB seedée pristine.
  • Contexte : suite vitest qui partage une DB Postgres unique (cas typique : maxWorkers: 1 ou DB unique côté CI).
  • Quand l'utiliser : projet avec ≥50 fichiers de tests partageant une DB, présence de tests qui mutent les données seed sans restaurer systématiquement.
  • Quand l'éviter : si les tests utilisent transactions+rollback (déjà isolés), DB-per-worker, ou si le rôle Postgres n'a pas le droit CREATEDB.
  • Avantage :
    • flakiness inter-fichiers éliminée par construction
    • les tests ne polluent plus la DB de dev partagée
    • réutilisation entre sessions : ~27 s au 1er run, < 1 s aux suivants
  • Limites / vigilance :
    • surcoût ~50 s sur 134 fichiers (cycle drop + create FROM TEMPLATE ~380 ms par fichier)
    • les sub-processes (prisma db seed) doivent recevoir les envs critiques explicitement (APP_BASE_URL, JWT_SECRET, ENCRYPTION_KEY)
  • Validé le : 01-05-2026
  • Contexte technique : Prisma 7 / pg 8 / Postgres 17 / vitest 4 — RL799_V2

Mécanique en 3 lots

Lot 1 — DSN + primitives admin SQL (dbUrls.ts, dbAdmin.ts) : getAdminDsn(), getTemplateDsn(), getTestDsn(), databaseExists, dropDatabase (pg_terminate_backend + DROP IF EXISTS), createDatabase(name, { template }). Whitelist stricte des noms de DB (les SQL admin ne supportent pas $1).

Lot 2 — Bootstrap idempotent du template (bootstrapTemplate.ts) :

export async function ensureTemplateReady() {
  if (await databaseExists(TEMPLATE)) return;       // no-op si déjà là
  await createDatabase(TEMPLATE);
  spawnSync('pnpm', ['-C', 'apps/api', 'exec', 'prisma', 'migrate', 'deploy'], {
    env: { ...process.env, DB_URL: getTemplateDsn() },
  });
  spawnSync('pnpm', ['-C', 'apps/api', 'exec', 'prisma', 'db', 'seed'], {
    env: { ...process.env, DB_URL: getTemplateDsn() },
  });
}

Lot 3 — Câblage vitest :

// globalSetup.ts
export default async function globalSetup() {
  await ensureTemplateReady();
  if (await databaseExists(TEST)) await dropDatabase(TEST);
  return async () => {
    if (await databaseExists(TEST)) await dropDatabase(TEST);
  };
}

// setupFile.ts (avant chaque fichier)
if (await databaseExists(TEST)) await dropDatabase(TEST);
await createDatabase(TEST, { template: TEMPLATE });

Pièges anticipés

  • resolveDbUrl() qui force le pathname /test : doit préserver le DSN s'il pointe déjà sur la template (sinon le seed sub-process écrit dans la mauvaise DB).
  • prisma db seed sub-process : les envs critiques chargées par lib/prisma.ts côté app doivent être propagées au sub-process (loadEnv.ts importé en tête de globalSetup.ts).
  • globalForPrisma cache un PrismaClient : c'est le contenu de la DB qui change (drop+create), pas le DSN — pas de $disconnect() nécessaire grâce à pg_terminate_backend.

Checklist

  • Whitelist stricte des noms de DB autorisés (anti-injection)
  • Bootstrap idempotent (réutilise la template entre sessions de dev)
  • Sub-processes du bootstrap reçoivent les envs critiques
  • Drop + create FROM TEMPLATE au début de chaque fichier
  • Test de flakiness avant/après pour valider le gain

Pattern : Helper waitForX() polling-borné pour les attentes fire-and-forget

  • Objectif : remplacer les setTimeout(50) + findFirst dispersés par un helper centralisé robuste à la charge CPU et auto-documenté.
  • Contexte : tests qui valident un side-effect async (audit log, notification, mail log) déclenché par setImmediate / Promise.resolve().then(...) côté SUT.
  • Quand l'utiliser : tout test qui attend qu'un side-effect non-awaited apparaisse en DB.
  • Quand l'éviter : pour vérifier l'absence d'un event — là il faut un délai fixe puis assertion d'absence (le polling jusqu'au timeout ne prouve rien).
  • Avantage :
    • robuste aux variations de charge (pas de durée arbitraire)
    • fail rapide si l'event n'arrive pas (timeout 1500 ms par défaut)
    • lisibilité : intention claire (waitForX vs setTimeout + findFirst)
  • Limites / vigilance :
    • le polling consomme des requêtes DB (1 toutes les 50 ms) — négligeable en maxWorkers: 1
    • NE PAS utiliser pour vérifier l'absence d'un event
  • Validé le : 25-04-2026
  • Contexte technique : Vitest / Prisma — RL799_V2

Implémentation

// __tests__/helpers/asyncWait.ts
export const waitForAudit = async (
  query: { action: string; targetId?: string; userId?: string },
  options: { timeoutMs?: number; intervalMs?: number } = {},
) => {
  const timeout = options.timeoutMs ?? 1500;
  const interval = options.intervalMs ?? 50;
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    const audit = await prisma.auditLog.findFirst({ where: query });
    if (audit) return audit;
    await new Promise((r) => setTimeout(r, interval));
  }
  return null;
};

Anti-pattern

// ❌ délai arbitraire et fragile
await new Promise((r) => setTimeout(r, 50));
const notif = await prisma.notification.findFirst({ where: { ... } });

// ✅ polling borné, intention claire
const notif = await waitForNotification({ type: 'X', recipientId: userId });

Checklist

  • Filtre exhaustif (au minimum recipientId ou targetId pour éviter de matcher les artefacts d'un autre test)
  • Timeout par défaut court (1500 ms)
  • Migration progressive — pas tous les tests d'un coup

Pattern symétrique pour vérifier l'ABSENCE d'un side-effect

waitForX ne convient pas pour prouver qu'aucun event n'apparaît : le polling jusqu'au timeout ne distingue pas "aucun event" de "event arrivé après le timeout". Le remplacer par setTimeout(100) + count() souffre d'une double fragilité (trop court en CI lent → faux négatif si l'event fuyant arrive après ; trop long → temps gaspillé).

Le helper symétrique correct est un polling-borné fail-fast (assertCountStable) : il poll-vérifie que le compteur reste à la valeur attendue sur une fenêtre courte, et abandonne dès qu'un compteur surnuméraire est observé.

// __tests__/helpers/asyncWait.ts
export const assertNotificationCountStable = async (
  query: { type: NotificationType; recipientId?: string },
  expected: number,
  options: { windowMs?: number; intervalMs?: number } = {},
): Promise<number> => {
  const window = options.windowMs ?? 200;
  const interval = options.intervalMs ?? 50;
  const deadline = Date.now() + window;

  let count = await prisma.notification.count({ where: query });
  if (count > expected) return count; // fail-fast immédiat

  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, interval));
    count = await prisma.notification.count({ where: query });
    if (count > expected) return count; // fail-fast
  }
  return count;
};
// ❌ délai arbitraire — trop court en CI lent (faux négatif), trop long = temps perdu
await new Promise((r) => setTimeout(r, 100));
expect(await prisma.notification.count({ where: { /* … */ } })).toBe(1);

// ✅ polling borné, fail-fast si une notif fuyante apparaît
const count = await assertNotificationCountStable({ type: 'X', recipientId: userId }, 1);
expect(count).toBe(1);

Checklist (absence) :

  • Fenêtre courte (200 ms par défaut — durée typique d'un fire-and-forget non-désiré, pas un timeout)
  • Filtre exhaustif — recipientId ou targetId au minimum
  • Le test affirme count === expected APRÈS le helper, pas pendant

Cas vécu : PATCH/DELETE payments → aucune nouvelle notif (cotisationsPayments.test.ts), 05-05-2026.


Pattern : Injection de dépendance testable via hooks module-level __setXForTests / __resetXForTests

  • Objectif : surcharger en test une dépendance d'un module de prod (getter de clés JWKS, sub-resolver DB, …) SANS polluer la signature publique (param de DI) ni recourir à vi.mock (fragile sur les imports transitifs, hoisting capricieux).
  • Contexte : module dont une dépendance interne doit varier en test mais dont la signature publique est un invariant ("signatures intouchables").
  • Quand l'utiliser : la dépendance est interne et vi.mock se révèle fragile (chaîne d'imports transitifs).
  • Quand l'éviter : la dépendance est déjà un paramètre explicite de la fonction (param-threading suffit) ; ou un vi.mock simple et stable fait l'affaire.
  • Avantage :
    • pas de param de DI dans le contrat public, pas de vi.mock fragile
    • bonus diagnostic : injecter un fake qui throw si appelé prouve qu'un chemin n'est PAS pris (ex. "le sub-resolver Keycloak ne doit pas être appelé quand le JWT-maison gagne")
  • Limites / vigilance :
    • reset systématique dans setupFile.ts avant chaque fichier (même garde-fou que les rate-limiters in-memory : un fichier qui injecte ne doit pas polluer le suivant → flakiness inter-fichiers évitée)
    • le défaut de PRODUCTION reste le comportement réel (ex. stub fail-closed () => null tant que la vraie impl n'existe pas)
  • Validé le : 14-06-2026
  • Contexte technique : Vitest — RL799_V2 (rate-limiters, puis Keycloak K1.1)

Implémentation

// module de prod
let active = productionDefault;

export function __setXForTests(v: typeof productionDefault): void { active = v; }
export function __resetXForTests(): void { active = productionDefault; }
// setupFile.ts — reset AVANT chaque fichier
beforeEach(() => {
  __resetXForTests();
  __resetAllRateLimitersForTests();
});

Cas vécus : __resetAllRateLimitersForTests (rateLimiter.ts), puis __setKeycloakKeyGetterForTests / __setSubResolverForTests (K1.1).


Pattern : e2e DB-based NestJS + Prisma v7 (Jest + @swc/jest)

  • Objectif : exécuter des e2e Jest contre une vraie base Postgres (sans mocker PrismaClient) dans un projet NestJS utilisant Prisma v7.
  • Contexte : Prisma v7 charge dynamiquement un runtime WASM (await import('....mjs'), import.meta.url) → incompatible avec la config Jest historique ts-jest + moduleNameMapper qui mock le client.
  • Quand l'utiliser : projet NestJS + Prisma v7 voulant des e2e avec persistance réelle (validation de contracts Zod réels, fixtures partagées avec le seed, intégrité cross-modules).
  • Quand l'éviter : suites qui n'ont pas besoin d'une vraie DB → garder le pipeline mocké (plus rapide, plus isolé).
  • Avantage :
    • exploite des builders typés contre une vraie base
    • pattern inter-projet : tout NestJS + Prisma v7 rencontre le même problème
  • Limites / vigilance :
    • garde-fou anti-truncate destructeur obligatoire (refuser si DATABASE_URL ne contient pas "test")
    • PAS de mock global argon2 (sinon le hash est stub et les tests d'auth ne valident rien)
  • Validé le : 27-05-2026
  • Contexte technique : NestJS / Jest / @swc/jest / Prisma v7 / Postgres — app-alexandrie

Config Jest e2e DB-based minimale

{
  "moduleFileExtensions": ["js", "mjs", "json", "ts"],
  "testRegex": ".e2e-db-spec.ts$",
  "setupFiles": ["<rootDir>/test-env-db.ts"],
  "transformIgnorePatterns": ["/node_modules/(?!(@prisma)/)"],
  "transform": {
    "^.+\\.(t|j|mj)s$": ["@swc/jest", {
      "jsc": {
        "target": "es2023",
        "parser": { "syntax": "typescript", "decorators": true, "dynamicImport": true },
        "transform": { "legacyDecorator": true, "decoratorMetadata": true },
        "keepClassNames": true
      },
      "module": { "type": "commonjs" }
    }]
  },
  "moduleNameMapper": { "^(\\.{1,2}/.*)\\.js$": "$1" }
}

Points-clés :

  • @swc/jest au lieu de ts-jest : gère import.meta.url, await import() dynamique, et respecte decoratorMetadata (DI Nest).
  • transformIgnorePatterns ouvert pour /node_modules/@prisma/ : les .mjs du runtime WASM doivent passer dans le transformer.
  • moduleNameMapper strip les extensions .js : Prisma v7 écrit ses imports en ESM strict (./internal/class.js), Jest CJS résout .ts.
  • PAS de moduleNameMapper mappant PrismaClient vers un mock (sinon on tape le mock, pas la vraie DB).

Helper e2e avec garde-fou anti-truncate

// _helpers/e2e-db.ts
export async function truncateE2EDb(prisma: PrismaClient): Promise<void> {
  const url = process.env.DATABASE_URL ?? '';
  if (!url.includes('test')) {
    throw new Error('[e2e-db] truncate refusé : DATABASE_URL ne contient pas "test"');
  }
  await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE;`);
}

Infra (à provisionner une fois) : DB dédiée avec "test" dans le nom + DATABASE_URL=...test pnpm exec prisma migrate deploy.

Cohabitation avec les e2e mockés historiques

  • Garder le pipeline mocké pour les suites sans besoin de vraie DB.
  • Migrer progressivement vers le DB-based les e2e qui bénéficient d'une vraie persistance.
  • Convention de nommage : *.e2e-spec.ts (mockés) vs *.e2e-db-spec.ts (DB-based) — extension distinctive captée par les testRegex respectifs.

Pattern : Tester l'atomicité d'une transaction (audit + mutation)

  • Objectif : prouver par un test que la promesse "X est garanti par une transaction" tient — sans ce test, un refactor "fire-and-forget" passe en review sans alarme.
  • Contexte : code de service qui revendique mutation persistée ⇔ audit log existe via prisma.$transaction(async tx => { await tx.X.update(...); await logActionSync(tx, ...); }).
  • Quand l'utiliser : sur tout chemin critique où l'audit ou un side-effect transactionnel est un livrable métier.
  • Quand l'éviter : pour les audits purement informatifs (statistiques d'usage) où le fire-and-forget est acceptable.
  • Avantage :
    • le test fait office de contrat exécutable du pattern transactionnel
    • une régression silencieuse (passer prisma au lieu de tx) casse le test immédiatement
  • Limites / vigilance :
    • le mock doit cibler le seul appel ciblé (pas un mock global qui fait tout throw)
  • Validé le : 27-04-2026
  • Contexte technique : Vitest / Prisma — RL799_V2

Implémentation (recipe vitest + spyOn)

import * as auditRepository from '../repositories/admin/auditRepository';

test('mutation rollback si audit throw', async () => {
  vi.spyOn(auditRepository, 'createAuditLog').mockImplementation(async (data) => {
    if (data.action === 'X.cross_soiree_update') {
      throw new Error('audit-failure-simulee');
    }
    return null as never;
  });

  const res = await POST_HANDLER(...);
  assert.ok(res.status >= 500);
  assert.equal(await prisma.X.findFirst(...), null);  // rollback
  assert.equal(await prisma.auditLog.count(...), 0);
});

Checklist

  • Mock chirurgical sur l'action ciblée, pas un mock global
  • Assertions sur trois invariants : status HTTP 5xx, donnée non persistée, audit non écrit
  • Restaurer le mock après le test (vi.restoreAllMocks() en afterEach)

Pattern : Convention describe() minimale pour tests d'intégration

  • Objectif : rendre les fichiers de tests > 150 LOC navigables, regroupables par cause d'échec et lisibles dans le reporter.
  • Contexte : tests d'intégration API où les assertions s'enchaînent sans regroupement, ou fichiers historiquement plats.
  • Quand l'utiliser : tout fichier > 150 LOC ou > 5 tests.
  • Quand l'éviter : fichier court (< 50 LOC, < 3 tests) où le describe ajoute du bruit.
  • Avantage :
    • le reporter vitest groupe les échecs logiquement (toutes les erreurs d'auth ensemble)
    • navigation facilitée par recherche (describe('Auth')
    • un groupe devient candidat naturel à l'extraction en fichier dédié
  • Limites / vigilance :
    • 2 niveaux maximum — au-delà ça devient illisible
  • Validé le : 24-04-2026
  • Contexte technique : Vitest — RL799_V2

Convention recommandée (2 niveaux)

describe('POST /api/tenues', () => {
  describe('Auth', () => {
    test('refuse un user non-admin → 403', async () => { /* … */ });
    test('refuse un token invalide → 401', async () => { /* … */ });
  });
  describe('Validation', () => {
    test('refuse un body sans date → 400', async () => { /* … */ });
  });
  describe('Happy path', () => {
    test('crée une tenue valide → 201', async () => { /* … */ });
  });
});

Seuils

  • < 50 LOC et < 3 tests : optionnel
  • 50150 LOC : 1 niveau de contexte
  • > 150 LOC ou > 5 tests : 2 niveaux obligatoires

Niveau 2 — vocabulaire stable

Auth, Validation, Happy path, Edge cases, Error handling, Integration. Ne pas inventer un nom local — la cohérence inter-fichiers vaut plus que l'expressivité ponctuelle.


Pattern : Refactor itératif d'un fichier de tests monolithe

  • Objectif : découper un fichier > 1000 LOC mêlant plusieurs domaines métier en fichiers thématiques sans introduire de régression.
  • Contexte : fichier de tests historique qui partage un beforeEach lourd, n'a pas de describe, et accueille tous les nouveaux tests par défaut.
  • Quand l'utiliser : > 1000 LOC, plusieurs domaines mélangés, navigation pénible.
  • Quand l'éviter : fichier ciblé, déjà cohérent même volumineux.
  • Avantage :
    • 1 commit par domaine extrait → review possible commit par commit
    • helpers réutilisables émergent naturellement
    • reporter vitest groupe les échecs par domaine
  • Limites / vigilance :
    • flakiness inter-fichiers possible pendant la transition (cleanups incomplets temporaires)
    • tolérer 1-2 fails au 1er run pendant le chantier, investiguer après
  • Validé le : 25-04-2026
  • Contexte technique : Vitest — RL799_V2

Étapes (ordre obligatoire)

  1. Analyse : grep -nE "^test|^// ---" fichier.test.ts → carte des sections + repérage des dépendances closure.
  2. Extraction des helpers d'abord : helpers/<domaine>.ts avec setup<Domaine>Fixtures(cleanup) qui retourne explicitement les IDs (pas de closure module-level).
  3. POC sur le domaine le plus simple (4-7 tests, peu de dépendances). Commit + push dès que vert.
  4. Itération domaine par domaine par ordre croissant de complexité. Une seule règle : ne jamais commencer un nouveau domaine sans avoir commit le précédent.
  5. Suppression finale du fichier monolithe quand le dernier domaine est extrait.

Pièges à éviter

  • Ne jamais dupliquer le fichier pour le scinder : toujours déplacer (extraction → suppression dans l'original → commit).
  • Closure variables partagées : si les tests utilisent let X = '' au niveau module, les ids doivent passer par le retour de setup<Domaine>.
  • Snapshot/restore seed : si le fichier original prenait un snapshot d'entries seed, chaque nouveau fichier doit le faire (sinon le 1er fichier qui tourne snapshot un état déjà pollué).

Checklist

  • Helpers extraits avant le 1er domaine
  • 1 commit par domaine
  • wc -l du fichier original baisse à chaque commit (preuve de progrès)
  • Suite verte à chaque commit