Apparence
A3 — Permissivité référentiel ↔ campagnes
🆕 Évolution proposée — remplace le modèle « référentiel + snapshots » de §8
Statut : 🟢 prêt à spécifier. L'arbitrage structurant est tranché (voir A3bis) : c'est la décision la plus lourde de toute la section.
En une phrase
Aujourd'hui, le périmètre d'une campagne est décidé une fois pour toutes au moment où on l'ouvre, et il n'est plus modifiable ensuite sans détruire des résultats. La proposition : le rendre ajustable en cours de campagne, tout en garantissant qu'un test déjà joué ne peut plus changer de définition.
Constat — d'où vient exactement la rigidité
- Le périmètre est figé à l'ouverture de la campagne. L'activation recopie l'arbre du plan de test une seule fois ; une garde empêche toute recopie ultérieure.
- Un flux déjà présent au référentiel ne peut pas être ajouté à une campagne en cours. Il n'existe que trois façons pour un flux d'entrer dans une campagne, et toutes passent par sa création : créer le flux en vue campagne (avec ou sans ajout au référentiel), ou le créer au référentiel — auquel cas il n'apparaîtra que dans la campagne suivante.
- Retirer un flux d'une campagne, c'est le supprimer — et avec lui ses étapes, ses anomalies et ses preuves. On détruit des résultats de test. La notion « hors périmètre » n'existe pas.
Ce que ça donne sur le terrain. Une campagne démarre, et deux semaines plus tard le client signale un processus oublié. Aujourd'hui il faut soit le recréer entièrement dans la campagne (donc en double au référentiel), soit attendre la campagne suivante. Et si un flux se révèle hors sujet, le retirer efface les tests déjà passés dessus — donc on le garde et il plombe le taux d'avancement jusqu'à la fin.
Références au code v1.
| Constat | Où |
|---|---|
Copie unique à l'activation, garde d'idempotence snapshot_created_at (RG-14) | activate_campaign, migration 043 |
Les trois chemins d'entrée (create-dual, create-ref-only, campagne-only) | app/synthese/SyntheseContainer.jsx:108–190 |
Retrait = deleteTest → suppression en cascade | idem |
Ironie du modèle : la table campaign_tests (M2M campagne ↔ test) existe déjà en base mais est vestigiale (D9, §24) — c'est pourtant exactement le modèle qui aurait permis l'association souple.
Proposition fonctionnelle
- Ajouter au périmètre — sélection multiple depuis l'arbre du référentiel (cases à cocher + « Ajouter à la campagne … »), avec aperçu d'impact : nombre de flux et d'étapes ajoutés, effet sur le % d'avancement. Opération idempotente : un flux déjà au périmètre est refusé, pas dupliqué.
- Retirer du périmètre — retrait logique, jamais suppression : les résultats déjà saisis sont conservés, le flux sort des dénominateurs et de l'affichage, et peut être réintégré.
- Traçabilité obligatoire : toute entrée ou sortie de périmètre est journalisée (qui, quand, quoi). Sans cela, un % d'avancement devient inexplicable a posteriori — c'est la contrepartie non négociable de la souplesse.
Le glisser-déposer est possible mais non requis en première version : coût d'implémentation élevé pour un gain faible face à une sélection multiple.
Mutations à écrire.
campaigns.addTestsFromReferentiel(campaignId, testIds[])— idempotence garantie par l'index d'unicitéby_campaign_testsurcampaignTests.campaigns.removeTestFromScope(campaignId, testId)— poseexcludedAt, ne supprime rien.
Droits
admin + consultant, uniquement sur une campagne active. Une campagne clôturée reste immuable.
La règle d'état (« campagne clôturée = lecture seule ») est portée par la couche ABAC (canInContext), pas par l'UI — voir Architecture § autorisation.
Aligné marché
Xray sépare Test Repository (organisation) et Test Plan / Test Execution (périmètre exécuté) ; TestRail construit un test run par sélection de cas et ne fige qu'à la fermeture du run. Aucun des outils étudiés n'impose de figer le périmètre à la création.
A3bis — Arbitrage tranché : modèle hybride
✅ Décision du 19/07/2026 — clôt Q17
Ni copie intégrale (v1), ni association pure : association pour la structure, copie pour les seuls champs qui doivent rester immuables. Cette décision remplace le modèle « référentiel + snapshots » de §8 et éclaire Q5.
Le principe, en clair
Le plan de test n'est plus recopié dans chaque campagne : une campagne est simplement une liste de flux qu'on lui a rattachés. Ce qui est copié — et donc figé — ce sont uniquement les libellés du test au moment où quelqu'un l'exécute.
Le moment du gel est la clé de tout :
| Avant qu'une étape soit testée | Après qu'elle a été testée |
|---|---|
| Les corrections apportées au plan de test descendent dans la campagne (faute de frappe, précision ajoutée) | La définition est scellée — plus rien ne la modifie |
| C'est la souplesse demandée | Le testeur a validé ce libellé précis : le dossier de recette reste opposable |
Alternatives écartées : figer au moment de l'ajout au périmètre (cela recopie tout d'avance et annule le bénéfice) ; figer à la clôture de la campagne, comme TestRail (cela laisse une fenêtre pendant laquelle des étapes déjà validées peuvent encore changer).
Schéma cible
ts
// RÉFÉRENTIEL — une seule ligne par flux, jamais dupliquée
tests: defineTable({
organizationId, projectId, scenarioId,
testCode, nom, statut, // Draft / À rédiger / Validé
regressionCore: v.boolean(), // noyau de non-régression (A6)
archivedAt: v.union(v.number(), v.null()), // masquage (A2)
}).index("by_project", ["projectId"]),
etapes: defineTable({ // DÉFINITION SEULE — plus de `resultat`
testId, position,
nom, description, jeu, attendu, crimCode,
}).index("by_test", ["testId", "position"]),
// PÉRIMÈTRE — association
campaignTests: defineTable({
campaignId, testId, addedAt, addedBy,
excludedAt: v.union(v.number(), v.null()), // retrait logique, conserve les résultats
}).index("by_campaign", ["campaignId"])
.index("by_campaign_test", ["campaignId", "testId"]), // unicité → idempotence
// RÉSULTATS + GEL — 1 ligne par (campagne, étape), créée à la 1re saisie
etapeResults: defineTable({
campaignId, etapeId, testId,
resultat, gravite, commentaire, executePar, executeAt,
frozen: v.object({ testNom, position, nom, description, jeu, attendu, crimCode }),
}).index("by_campaign_etape", ["campaignId", "etapeId"])
.index("by_campaign", ["campaignId"]),Règle de lecture (unique) : une étape affichée dans une campagne = frozen si un etapeResult existe, sinon la définition vivante.
Ce que la décision impose d'écrire
- Dénominateur de couverture — ce n'est plus « toutes les lignes du snapshot » :
total = étapes vivantes des tests liés non exclus ∪ étapes ayant un etapeResult dans cette campagne. L'union couvre le cas d'une étape supprimée du référentiel après avoir été exécutée : elle compte toujours, elle a été jouée. - Suppression dure interdite sur un flux ou une étape possédant un
etapeResult→ archivage forcé. - Anomalies et preuves se rattachent à l'
etapeResult, plus à l'etape. Unicité(campaignId, etapeId)au lieu deUNIQUE(etape_id).
Ce que la décision fait disparaître
| Aujourd'hui | Demain |
|---|---|
| Une procédure d'activation lourde qui recopie tout l'arbre du projet | Le simple rattachement des flux choisis |
| Des libellés recopiés à plusieurs endroits, resynchronisés par une boucle fragile (D5) | Une seule version de chaque libellé, corrigée une fois |
| La « proposition de correction », qui recopie un flux entier | Une création de flux normale, puis un rattachement |
| Le flux « campagne-only » | Concept supprimé : il n'existait que parce que le périmètre était rigide. Un flux créé au référentiel et rattaché à une seule campagne donne le même résultat — une case à cocher en moins dans l'écran de création |
| 400 flux × 8 étapes × 10 campagnes ≈ 32 000 lignes stockées | ~3 200 définitions + uniquement les résultats réellement saisis |
⚠️ Cette dernière ligne n'est pas qu'une question d'élégance. Une transaction Convex lit au plus 32 000 documents. Le modèle v1 amenait le volume
etapesexactement à cette limite dès dix campagnes. A3bis maintient le modèle dans les limites de la plateforme autant qu'il répond au besoin fonctionnel. Voir Exigences non-fonctionnelles § 14.1.
Deux conséquences à valider côté métier
- A2 — l'archivage devient obligatoire, il n'est plus optionnel : dès qu'un flux a été testé au moins une fois, il ne peut plus être supprimé — seulement archivé. C'est le prix de la conservation de l'historique.
- Une anomalie peut désormais exister sur la même étape dans deux campagnes différentes — ce que le modèle actuel interdit (correction au passage de la règle RG-07). C'est le comportement attendu : la même étape peut échouer en version N puis en version N+1, ce sont deux anomalies distinctes.