Recette de bascule : comparer l'app d'aujourd'hui à la base migrée
test-plan.md §22. Le dry run compare des comptages ; cette recette compare le contenu — un compte juste ne dit pas qu'un quiz a gardé ses questions ni qu'un article a gardé ses traductions. Six volets : comptages globaux, comptages par type de section (où se cachent les pertes qu'un total masque), collections filles, colonnes que la migration génère ou décide et qui n'ont aucun équivalent à comparer, comparaison à l'écran, et relecture des Skipped. À jouer juste après le run et avant la bascule DNS : c'est la seule fenêtre où les deux bases coexistent. Reliée depuis I7 du plan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
526bb904b1
commit
92d86fa066
@ -315,6 +315,8 @@ Les lots 1 et 2 sont terminés. **Le lot 3 et le lot médias sont tous les deux
|
|||||||
- **(f)** `IsQRCode`/`IsSearchText`/`IsSearchNumber` n'existent pas dans Mongo — `false` est le bon défaut. Contrôle inverse fait : les cinq réglages qui *existent* (`IsDate`, `IsHour`, `IsSectionImageBackground`, `RoundedValue`, `ScreenPercentageSectionsMainPage`) sont bien mappés sur `AppConfigurationLink`.
|
- **(f)** `IsQRCode`/`IsSearchText`/`IsSearchNumber` n'existent pas dans Mongo — `false` est le bon défaut. Contrôle inverse fait : les cinq réglages qui *existent* (`IsDate`, `IsHour`, `IsSectionImageBackground`, `RoundedValue`, `ScreenPercentageSectionsMainPage`) sont bien mappés sur `AppConfigurationLink`.
|
||||||
- **(g)** tombé avec le rename du lot B.
|
- **(g)** tombé avec le rename du lot B.
|
||||||
|
|
||||||
|
**Recette de bascule** : [test-plan.md §22](test-plan.md) — comparaison contenu par contenu entre l'app d'aujourd'hui et la base migrée, à jouer **pendant que Mongo est encore lisible**. Reliée depuis I7.
|
||||||
|
|
||||||
**Dry run** : automatisé en test (`MigrationDryRunTests`, se saute sans Mongo), joué sur l'export du 1ᵉʳ avril. Il a trouvé du premier coup **20 sections manquantes avec `Erreurs : 0`** — elles référencent 2 configurations supprimées dans Mongo (contenu MDLF). Pas un défaut de migration, mais un silence : elles sont désormais signalées dans `Skipped`. ⚠️ **Décision produit en attente** : recréer les 2 configurations avant la bascule pour récupérer ce contenu, ou acter sa perte. ⚠️ **À rejouer sur un dump frais** avant le jour J — l'export a 4 mois et la prod tourne encore sur Mongo.
|
**Dry run** : automatisé en test (`MigrationDryRunTests`, se saute sans Mongo), joué sur l'export du 1ᵉʳ avril. Il a trouvé du premier coup **20 sections manquantes avec `Erreurs : 0`** — elles référencent 2 configurations supprimées dans Mongo (contenu MDLF). Pas un défaut de migration, mais un silence : elles sont désormais signalées dans `Skipped`. ⚠️ **Décision produit en attente** : recréer les 2 configurations avant la bascule pour récupérer ce contenu, ou acter sa perte. ⚠️ **À rejouer sur un dump frais** avant le jour J — l'export a 4 mois et la prod tourne encore sur Mongo.
|
||||||
|
|
||||||
**Les cinq vrais, corrigés** :
|
**Les cinq vrais, corrigés** :
|
||||||
|
|||||||
129
test-plan.md
129
test-plan.md
@ -761,6 +761,135 @@ Points de vigilance à ne pas oublier au moment de tester :
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 22. Recette de bascule — l'app d'aujourd'hui vs la base migrée
|
||||||
|
|
||||||
|
> Ajoutée le 2026-08-11, à la demande. **À jouer juste après le `POST /api/migration/run` du jour J**, avant la bascule DNS et pendant que Mongo est encore lisible. C'est la seule fenêtre où les deux bases coexistent : après, la comparaison n'est plus possible.
|
||||||
|
>
|
||||||
|
> Le dry run compare des **comptages**. Cette recette compare le **contenu** — un compte juste ne dit pas qu'une section a gardé ses questions, ses traductions et ses médias.
|
||||||
|
>
|
||||||
|
> Rappel : `MigrationController` lit un MongoDB **vivant**, pas les fichiers de `migration-data/`.
|
||||||
|
|
||||||
|
### 22.1 — Comptages globaux, les deux bases côte à côte
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mongo (source) — adapter l'URI au Mongo de prod ou au dump restauré
|
||||||
|
docker run --rm --network container:myim_mongo_dryrun mongo:6 \
|
||||||
|
mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval '
|
||||||
|
["Instances","Users","Configurations","Resources","Sections","Devices"]
|
||||||
|
.forEach(c => print(c.padEnd(16) + db[c].countDocuments()));'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Postgres (cible)
|
||||||
|
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||||||
|
SELECT 'Instances' AS entite, count(*) FROM "Instances"
|
||||||
|
UNION ALL SELECT 'Users', count(*) FROM "Users"
|
||||||
|
UNION ALL SELECT 'Configurations', count(*) FROM "Configurations"
|
||||||
|
UNION ALL SELECT 'Resources', count(*) FROM "Resources"
|
||||||
|
UNION ALL SELECT 'Sections', count(*) FROM "Sections"
|
||||||
|
UNION ALL SELECT 'Devices', count(*) FROM "Devices";
|
||||||
|
SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
| # | Contrôle | Attendu | OK |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 22.1.1 | Instances, Users, Configurations, Resources, Devices | **Identiques** des deux côtés | |
|
||||||
|
| 22.1.2 | Sections | Postgres = Mongo **moins les orphelines signalées**. Le rapport les liste dans `Skipped` : leur nombre doit expliquer *exactement* l'écart | |
|
||||||
|
| 22.1.3 | `FatalError` et code HTTP | Vide, et **200**. Un 500 veut dire que la transaction a été annulée : rien n'a été écrit, on corrige et on rejoue | |
|
||||||
|
|
||||||
|
### 22.2 — Sections par type : c'est là que les pertes se cachent
|
||||||
|
|
||||||
|
Un compte global juste peut masquer 19 articles perdus compensés ailleurs. À comparer **type par type**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mongo — Type est un entier : 0=Map 1=Slider 2=Video 3=Web 4=Menu 5=Quiz
|
||||||
|
# 6=Article 7=PDF 8=Game 9=Agenda 10=Weather
|
||||||
|
docker run --rm --network container:myim_mongo_dryrun mongo:6 \
|
||||||
|
mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval '
|
||||||
|
db.Sections.aggregate([{$group:{_id:"$Type",n:{$sum:1}}},{$sort:{_id:1}}])
|
||||||
|
.forEach(r => print(r._id + " : " + r.n));'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Postgres — héritage TPH, le type vit dans Discriminator
|
||||||
|
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||||||
|
SELECT "Discriminator", count(*) FROM "Sections" GROUP BY 1 ORDER BY 1;
|
||||||
|
SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
| # | Contrôle | Attendu | OK |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 22.2.1 | Chaque type de Mongo se retrouve en Postgres | Écart nul, **ou** entièrement expliqué par les orphelines de ce type | |
|
||||||
|
| 22.2.2 | `Event` et `Parcours` | **0 des deux côtés** — ces types n'existaient pas dans Mongo. En voir apparaître signalerait une erreur de mapping | |
|
||||||
|
| 22.2.3 | `Game` | Autant que de type 8 dans Mongo, avec `GameType = Puzzle` | |
|
||||||
|
|
||||||
|
### 22.3 — Les collections filles, invisibles dans les comptages de sections
|
||||||
|
|
||||||
|
C'est l'écart (b) : un quiz peut arriver sans une seule question sans que rien ne le signale.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||||||
|
SELECT count(*) AS questions,
|
||||||
|
count(DISTINCT "SectionQuizId") AS quiz_pourvus
|
||||||
|
FROM "QuizQuestions";
|
||||||
|
SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
| # | Contrôle | Attendu | OK |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 22.3.1 | Total `QuizQuestions` | **41** sur l'export d'avril — à recalculer sur le dump du jour J | |
|
||||||
|
| 22.3.2 | Ouvrir un quiz migré dans manager-app | Ses questions sont là, **avec leurs réponses et la bonne réponse cochée** | |
|
||||||
|
| 22.3.3 | Ordre des réponses | Identique à l'ancienne app | |
|
||||||
|
| 22.3.4 | `EventAgendas` et `GuidedPaths` | **Vides** — Mongo n'en contient pas. Non vides = un mapping a inventé des données | |
|
||||||
|
|
||||||
|
### 22.4 — Ce que la migration génère ou décide
|
||||||
|
|
||||||
|
Ces colonnes n'existent pas dans Mongo : rien à confronter, tout à vérifier. **C'est ici que se logent les pannes silencieuses du §1quinquies.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||||||
|
SELECT "Name", "WebSlug", left("PublicApiKey", 6) AS cle, "SubscriptionPlanId",
|
||||||
|
"AiTokensPerMonth", "IsMobile", "IsTablet", "IsWeb", "IsAssistant"
|
||||||
|
FROM "Instances" ORDER BY "Name";
|
||||||
|
SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
| # | Contrôle | Attendu | OK |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 22.4.1 | `PublicApiKey` | **Non nulle pour les 4 instances**, préfixe `ap_`. Nulle = les apps visiteur ne s'authentifient plus | |
|
||||||
|
| 22.4.2 | `WebSlug` | Non nul et **unique**, dérivé du nom | |
|
||||||
|
| 22.4.3 | `SubscriptionPlanId` | `plan-premium` pour MyInfoMate et VisitNamur, `plan-pro` pour MDLF et le Fort | |
|
||||||
|
| 22.4.4 | `AiTokensPerMonth` | > 0 sur les deux Premium. **0 sur les deux Pro, c'est voulu** — Pro n'inclut pas l'IA | |
|
||||||
|
| 22.4.5 | `IsMobile` / `IsTablet` | Cohérents avec les apps réellement utilisées par chaque client | |
|
||||||
|
| 22.4.6 | `IsWeb` | **Faux partout** — le canal web n'existait pas avant, il s'active à la main | |
|
||||||
|
| 22.4.7 | Rôles des utilisateurs | Tous `InstanceAdmin`. **Aucun `SuperAdmin` : à poser à la main sur ton compte**, sinon les écrans SuperAdmin sont inatteignables | |
|
||||||
|
| 22.4.8 | `Resources.StoragePath` | Renseigné pour les types fichier, **nul pour les types URL** (`ImageUrl`, `VideoUrl`, `JSONUrl`) | |
|
||||||
|
| 22.4.9 | `Resources.SizeBytes` à 0 | Uniquement celles listées dans les `Errors` du rapport (HEAD en échec). Le quota les comptera pour rien tant qu'un backfill ne passe pas | |
|
||||||
|
|
||||||
|
### 22.5 — Comparaison à l'écran, ancienne app vs nouvelle
|
||||||
|
|
||||||
|
Les comptages ne disent rien des traductions, des médias liés ni de l'ordre. **Prendre une configuration par instance** et les ouvrir côte à côte.
|
||||||
|
|
||||||
|
| # | Contrôle | Attendu | OK |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 22.5.1 | La liste des sections, dans l'ordre | Même ordre, mêmes libellés | |
|
||||||
|
| 22.5.2 | Une section Article | Contenu HTML identique, **dans chaque langue de la configuration** | |
|
||||||
|
| 22.5.3 | Une SectionMap | Mêmes points, mêmes catégories, même icône, même centrage et zoom | |
|
||||||
|
| 22.5.4 | Un Menu | Les sections liées sont les mêmes (`LinkMenuSectionsAsync` relie après coup) | |
|
||||||
|
| 22.5.5 | Un PDF | Le fichier s'ouvre, et c'est le bon | |
|
||||||
|
| 22.5.6 | Une sous-section | Toujours rattachée au bon parent | |
|
||||||
|
| 22.5.7 | Couleurs et loader de la configuration | Identiques | |
|
||||||
|
| 22.5.8 | **Une app visiteur avec sa vraie clé API** | Se connecte et affiche le contenu — le seul contrôle qui teste `PublicApiKey` de bout en bout | |
|
||||||
|
|
||||||
|
### 22.6 — Le contenu qu'on sait perdu
|
||||||
|
|
||||||
|
| # | Contrôle | Attendu | OK |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 22.6.1 | Relire les `Skipped` du rapport | Chaque ligne est une section orpheline **connue et acceptée**. Une surprise ici = on arrête et on regarde | |
|
||||||
|
| 22.6.2 | Les 20 sections MDLF orphelines | Soit récupérées (les 2 configurations recréées dans Mongo **avant** la bascule), soit leur perte **actée avec le client** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Hors scope (non implémenté)
|
## Hors scope (non implémenté)
|
||||||
|
|
||||||
| Feature | Statut |
|
| Feature | Statut |
|
||||||
|
|||||||
@ -306,7 +306,7 @@ Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le text
|
|||||||
|
|
||||||
**4. Écart restant, hors bascule** : la FAQ de `myinfomate-landing` (`src/data/segments.ts`) appelle encore le plan à 179€ « **Bundle** » — **41 occurrences en 4 langues** — alors que les cartes de tarifs disent « Premium ». Un prospect qui lit la grille puis la FAQ voit deux noms pour la même offre. Chantier de texte commercial, sans effet sur la migration.
|
**4. Écart restant, hors bascule** : la FAQ de `myinfomate-landing` (`src/data/segments.ts`) appelle encore le plan à 179€ « **Bundle** » — **41 occurrences en 4 langues** — alors que les cartes de tarifs disent « Premium ». Un prospect qui lit la grille puis la FAQ voit deux noms pour la même offre. Chantier de texte commercial, sans effet sur la migration.
|
||||||
|
|
||||||
| I7 | Vérifications post-bascule : login manager-app, une app visiteur **avec sa clé API**, un parcours, une carte, un PDF, un quiz avec ses questions | Les écarts b et c ne se voient qu'ici |
|
| I7 | **Recette de bascule — [test-plan.md §22](test-plan.md)**, ajoutée le 2026-08-11. Comptages globaux, puis **par type de section**, collections filles, colonnes générées, et comparaison à l'écran ancienne app vs nouvelle. ⚠️ **À jouer pendant que Mongo est encore lisible** : c'est la seule fenêtre où les deux bases coexistent, après la comparaison est impossible | Le dry run compare des **comptages**, le §22 compare le **contenu** — un compte juste ne dit pas qu'un quiz a gardé ses questions ni qu'un article a gardé ses traductions. Les écarts b et c ne se voient qu'ici |
|
||||||
| I8 | Bascule DNS / API, Mongo gardé en lecture seule quelques jours, `pg_dump` après | |
|
| I8 | Bascule DNS / API, Mongo gardé en lecture seule quelques jours, `pg_dump` après | |
|
||||||
|
|
||||||
### Après la prod
|
### Après la prod
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user