Files
Omika/docs/E2E_SYNC_VALIDATION.md
2026-10-23 18:46:50 +02:00

787 lines
38 KiB
Markdown

# E2E SYNC VALIDATION — Test end-to-end réel du pipeline de synchronisation
## Statut : EXÉCUTÉ — CREATE VALIDÉ, REPLAY À RETESTER, IDEMPOTENCE RETRY CORRIGÉE TRANSACTIONNELLEMENT
> **Mise à jour 13/08/2026 17:27 — tentative du test Replay corrigé :**
> le test `Replay_SameOperationId_IsIdempotent` (corrigé, autonome, polling
> terminal) a été **ré-exécuté physiquement** (garde-fous `true`, Worker externe
> actif) mais a **échoué au polling du premier POST** : `Assert.NotNull() Failure:
> Value is null` (ligne 302). Le log du Worker ne montre **aucune ligne de
> traitement** d'opération pendant le test : le Worker externe n'a **pas traité**
> l'opération dans le délai de 30 s du test. Le statut API est donc resté non
> terminal. **Aucune écriture métier supplémentaire n'a été produite par cette
> tentative** (le Worker n'a rien traité). Les garde-fous ont été **restaurés à
> `false`** après la tentative. Le test Replay corrigé reste **à retester
> physiquement** (le Worker externe doit traiter l'opération dans le délai du
> test).
> **Mise à jour 13/08/2026 17:43 — tentative avec polling Worker E2E = 5 s :**
> le test Replay corrigé a été **ré-exécuté physiquement** avec un Worker externe
> lancé avec l'override temporaire `Sync__RetryDelaySeconds=5` (poll immédiat puis
> toutes les 5 s) et les garde-fous `true`. **Le pipeline a réellement atteint le
> premier SUCCESS** (le test a passé l'assertion `Assert.NotNull(firstStatus)` à la
> ligne 302). Le test a ensuite **échoué sur un bug de disposition du helper
> `CountInterventionsAsync`** (ligne 177) : `System.InvalidOperationException :
> 'WebServiceHfSqlConnection' type only implements IAsyncDisposable. Use
> DisposeAsync to dispose the container.` Le helper utilise `using var scope =
> _factory.Services.CreateScope()` (disposition synchrone) alors que
> `WebServiceHfSqlConnection` n'implémente que `IAsyncDisposable` — le même
> problème `IAsyncDisposable` déjà corrigé dans `Program.cs`. **Ce n'est pas un
> échec du pipeline** : le premier POST a atteint SUCCESS (le Worker a traité
> l'opération, transaction BATIK_TEST, handler, MobLog SUCCESS, COMMIT). Le test
> a échoué uniquement sur le comptage métier (helper). Les garde-fous ont été
> **restaurés à `false`** et le Worker **arrêté** après la tentative. Le test
> Replay corrigé reste **à retester physiquement** après correction du helper
> `CountInterventionsAsync` (disposition asynchrone).
> **Mise à jour 13/08/2026 17:48 — micro-fix disposition + nouvelle tentative :**
> le helper `CountInterventionsAsync` a été corrigé (disposition asynchrone
> `await using var scope = _factory.Services.CreateAsyncScope()`), le projet de
> test compile. Le test Replay a été **ré-exécuté physiquement** (Worker E2E
> `Sync__RetryDelaySeconds=5`, garde-fous `true`). **Le pipeline a de nouveau
> atteint le premier SUCCESS** (le test est passé à la ligne 309). Le test a
> ensuite **échoué sur une nouvelle erreur du helper `CountInterventionsAsync`**
> (ligne 175) : `System.InvalidOperationException : La connexion Business doit
> être ouverte avant d'exécuter une commande.` (`BusinessOleDbConnection.EnsureOpen()`
> ligne 317). Le helper obtient `IWebServiceHfSqlConnection` et appelle
> `ExecuteScalarAsync` **sans ouvrir la connexion**. **Ce n'est pas un échec du
> pipeline** : le premier POST a atteint SUCCESS (transaction BATIK_TEST, handler,
> MobLog SUCCESS, COMMIT). Le test a échoué uniquement sur le comptage métier
> (helper). Les garde-fous ont été **restaurés à `false`** et le Worker **arrêté**
> après la tentative. Le test Replay corrigé reste **à retester physiquement**
> après ouverture de la connexion dans `CountInterventionsAsync`.
> **Mise à jour 13/08/2026 17:54 — micro-fix OpenAsync + E2E RÉUSSI :**
> le helper `CountInterventionsAsync` a été corrigé (ouverture explicite de la
> connexion `await connection.OpenAsync(cancellationToken)` avant
> `ExecuteScalarAsync`), le projet de test compile. Le test Replay a été
> **ré-exécuté physiquement** (Worker E2E `Sync__RetryDelaySeconds=5`, garde-fous
> `true`) et a **RÉUSSI** (1/1, 18 s) : premier POST → SUCCESS, business count = 1,
> second POST (même OperationId/RequestedEntityGuid/payload) → SUCCESS,
> CanonicalEntityGuid identique, business count final = 1, **aucun doublon**.
> **CREATE HFSQL PHYSIQUE : VALIDÉ. COMMIT TRANSACTIONNEL BATIK_TEST : VALIDÉ
> PHYSIQUEMENT. REPLAY TERMINAL : VALIDÉ PHYSIQUEMENT. IDEMPOTENCE TERMINALE :
> VALIDÉE.** Les garde-fous ont été **restaurés à `false`** et le Worker **arrêté**
> après la tentative. **Baseline Mobile : ÉCHEC** sur 2 tests transactionnels
> (`Crash_After_Commit_Does_Not_Reapply_Handler_On_Retry` ligne 403 et
> `Two_Different_OperationIds_Produce_Two_Business_Creations` ligne 436) :
> `Assert.Equal() Failure: Expected 1/2, Actual 0` — le handler n'a produit
> aucune création métier dans ces 2 tests unitaires. **STOP au premier échec de
> la baseline** : suite hors HFSQL et build solution non lancés.
> **Mise à jour 13/08/2026 18:02 — correction des 2 tests transactionnels rouges :**
> la cause de `Actual: 0` était un **défaut de test harness** (aucun défaut
> production) : les deux tests rouges utilisaient le handler générique
> `FakeSyncOperationHandler(SyncOperationResult.Success())`, qui retourne SUCCESS
> **sans écrire d'Intervention** dans le fake transactionnel. Le compteur
> `tx.CommittedInterventions` est alimenté par le handler via `WriteIntervention`
> (comme le font `ThrowingAfterWriteHandler` / `ThrowingThenSuccessHandler` des
> deux tests verts). Un handler dédié `SuccessWritingHandler` (écrit une
> Intervention + retourne SUCCESS + expose `CallCount`) a été ajouté et utilisé
> par les deux tests. Pour `Crash_After_Commit`, l'assertion `repo.Succeeded.Count
> == 1` a été remplacée par `Assert.Contains(env.OperationId, repo.Succeeded)` :
> au replay, le `SyncProcessor` reconstitue le log terminal SUCCESS (re-marquage
> via `MarkSuccessAsync`) **sans rappeler le handler** (comportement correct
> d'idempotence par OperationId) ; le fake `Succeeded` accumule donc un doublon,
> mais le handler n'est pas rappelé et aucune Intervention supplémentaire n'est
> durable. **Baseline complètement verte** : 4 tests transactionnels (4/4),
> SyncProcessor + SyncWorker (22/22), Mobile (515/515), suite hors HFSQL
> (1755/1755), build solution (0 erreur). **Aucun fichier production modifié.**
> **SYNC E2E PHYSIQUE reste VALIDÉ** (CREATE, COMMIT, REPLAY terminal,
> idempotence terminale). Garde-fous toujours `false`.
Le test end-to-end réel du pipeline de synchronisation **multi-connexion** a été
**réellement exécuté** (EXECUTED) contre les bases HFSQL de TEST. Le CREATE a
**réellement abouti** (SUCCESS, Intervention créée, MobLog SUCCESS).
Le test Replay **actuel** (`Replay_SameOperationId_IsIdempotent`) a été
**corrigé** pour devenir autonome : générer son propre OperationId, attendre
réellement SUCCESS, compter le métier = 1, re-POST exact, vérifier métier
toujours = 1. Il reste **à retester physiquement** (garde-fous `false`).
La **fenêtre de duplication retry** (business réussi mais terminal absent) a été
**fermée par une correction transactionnelle** : pour une opération réussie, les
écritures métier (Intervention), les relations associées, MobLog SUCCESS et la
finalisation MobReception sont désormais exécutées dans **UNE transaction OLE DB
locale BATIK_TEST partagée** (via `IWebServiceTransactionContext`). Soit tout est
durable, soit rien. Cette correction est **implémentée et testée** (tests
transactionnels verts), mais **pas encore validée par un E2E physique**.
**Résultat : SYNC E2E HFSQL PHYSIQUE (CREATE) : VALIDÉ.**
**REPLAY / IDEMPOTENCE TERMINALE : TEST CORRIGÉ, À RETESTER PHYSIQUEMENT.**
**IDEMPOTENCE RETRY : CORRECTION TRANSACTIONNELLE IMPLÉMENTÉE ET TESTÉE (pas encore E2E validée).**
**LICENCE E2E : NON VALIDÉE — BASE_LICENCES BLOQUÉE.**
## 0. Résultat de l'exécution réelle (13/08/2026)
### Maillons observés (dans l'ordre) — CREATE
| Maillon | Résultat | Détail |
|---|---|---|
| A. HTTP accepté | ✅ VERT | `POST /api/sync/operations` → 200 OK, `RECEIVED` |
| B. opération dans TOSRV | ✅ VERT | écrite dans `BATIK.01_BLS.TOSRV`, puis transférée |
| C. même OperationId | ✅ VERT | `receipt.OperationId == operationId` |
| D. TOSRV → MobReception | ✅ VERT | opération présente dans `BATIK_TEST.MobReception` |
| E. IDTOSRV conservé | ✅ VERT | `MR_IDToSRV` correspond à `IDTOSRV` source |
| F. SyncProcessor réel | ✅ VERT | `SyncProcessor.ProcessOperationAsync` appelé |
| G. Handler Intervention | ✅ VERT | `InterventionSyncOperationHandler.HandleCreateAsync` appelé |
| H. Intervention créée | ✅ VERT | Intervention `E2E_TEST_5b915439` créée dans BATIK_TEST |
| I. résultat dans MobLog | ✅ VERT | log `Success` écrit (SUCCESS) |
| J. status API SUCCESS | ✅ VERT | endpoint status → `SUCCESS` |
### Maillons observés — REPLAY
| Maillon | Résultat | Détail |
|---|---|---|
| HTTP accepté | ✅ VERT | réenvoi du même OperationId accepté |
| status API | ❌ ROUGE | retourne `Pending` au moment de la vérification (Worker pas encore passé) |
| Idempotence terminale | ❌ ROUGE | le test Replay **actuel** envoie les deux POST sans attendre SUCCESS : ce n'est pas un test d'idempotence terminale |
### Premier maillon rouge (Replay)
- **Maillon** : J — status API SUCCESS (Replay).
- **Exception/code HFSQL** : aucune erreur HFSQL. Le status est resté `Pending`
car le Worker externe n'a pas traité l'opération dans le délai du test
(~4 secondes).
- **Table** : N/A.
- **Colonne** : N/A.
- **Composant .NET** : `SyncPipelineE2ETests.Replay_SameOperationId_IsIdempotent`
(test conceptuellement incorrect : les deux POST sont envoyés immédiatement,
sans attendre le SUCCESS terminal du premier).
- **Correction appliquée** : le test Replay a été réécrit pour être autonome —
générer son propre OperationId, attendre réellement SUCCESS (polling terminal),
compter le métier = 1, re-POST exact, vérifier métier toujours = 1.
### Données E2E laissées
- **OperationId (CREATE)** : `5b915439-d9d9-44b5-9552-1f8f90db96c3`
- **RequestedEntityGuid (CREATE)** : `6679f54e-ace4-4df3-a75d-d69701601069`
- **CanonicalEntityGuid (CREATE)** : `0001014b-0001-0000-008a-2badf0d20f00`
- **OperationId (Replay)** : `0fce297f-e4e4-45ad-9027-b4e6e23d5c8a`
- **RequestedEntityGuid (Replay)** : `6f3bd0c1-8aeb-4296-9204-c7b1470c345a`
- **CanonicalEntityGuid (Replay)** : `0001014b-0001-0000-008a-2c339447ac00`
- **Interventions** : 7 lignes `E2E_TEST_*` laissées dans BATIK_TEST. Les 6
lignes `E2E_TEST_5b915439` sont des **doublons du MÊME OperationId**
`5b915439-d9d9-44b5-9552-1f8f90db96c3` (défaut d'idempotence retry business :
le handler CREATE a été rappelé à chaque retry, `retryCount=16` au SUCCESS,
recréant une Intervention à chaque tentative). La ligne `E2E_TEST_0fce297f`
est l'Intervention **normale** du test Replay (OperationId distinct
`0fce297f-e4e4-45ad-9027-b4e6e23d5c8a`), **PAS un doublon** du CREATE.
Aucune donnée personnelle réelle.
- **MobLog** : logs `Pending` + `Success` pour chaque opération.
- **TOSRV** : vide (opérations transférées et supprimées).
Aucun DELETE physique n'a été effectué. Un futur test ARCHIVE pourra servir au
cleanup des lignes `E2E_TEST_*` si nécessaire.
### Replay
Le test `Replay_SameOperationId_IsIdempotent` a été **réellement exécuté**
(EXECUTED) mais a **échoué** : le status est resté `Pending` au moment de la
vérification (le Worker externe n'a pas traité l'opération dans le délai du
test). Le Worker a néanmoins traité l'opération Replay **après** le test,
créant l'Intervention `E2E_TEST_0fce297f`.
**Diagnostic exact (micro-lot idempotence) :** le test Replay **actuel** génère
son **propre** OperationId (`0fce297f-e4e4-45ad-9027-b4e6e23d5c8a`) et son propre
RequestedEntityGuid (`6f3bd0c1-8aeb-4296-9204-c7b1470c345a`). Il ne réutilise
**pas** l'OperationId du test CREATE (`5b915439`). L'Intervention
`E2E_TEST_0fce297f` est donc l'Intervention **normale** du test Replay
(OperationId distinct), **PAS un doublon** du CREATE `5b915439`.
Le test Replay **actuel** est **conceptuellement incorrect** : il envoie les
deux POST immédiatement (lignes 213-218), **sans attendre le SUCCESS terminal
du premier**. Ce n'est donc pas un test d'idempotence terminale. Il a été
**corrigé** pour devenir autonome : générer son propre OperationId, attendre
réellement SUCCESS (polling terminal), compter le métier = 1, re-POST exact,
vérifier métier toujours = 1.
### Garde-fous
Les trois garde-fous ont été **restaurés à `false`** après l'exécution :
`AllowHfSqlWrites`, `ServiceDatabaseConfirmedTest`,
`WebServiceDatabaseConfirmedTest`. `AllowHfSqlWrites` n'est jamais laissé à
`true` après la tâche.
## 1. Architecture multi-connexion validée
```
ServiceConnection (base Service, table TOSRV)
↓
WebServiceConnection (base WebService/Business)
→ MobReception
→ MobLog
→ données métier (Intervention)
```
Le repository `SyncTransferRepository` est déjà adapté à cette topologie :
- `IServiceHfSqlConnection` → base Service, table `TOSRV`.
- `IWebServiceHfSqlConnection` → base WebService/Business, tables `MobReception`
et `MobLog`.
## 2. Environnement sans secrets
Ce document ne contient **aucun secret** : ni mot de passe, ni token, ni
ProtectionPlusSecret, ni FilePasswords, ni credential. Les valeurs de connexion
sont décrites par leur nom de clé et leur nature, jamais par leur valeur
sensible.
## 3. Bases réellement utilisées
### Service DB (base Service, table TOSRV)
| Section | Clé | Valeur effective |
|---|---|---|
| `ServiceConnection` | `DatabaseName` | `BATIK.01_BLS` (base Service actuelle) |
La base Service est utilisée par **Omika.Api** et **Omika.Worker** via la
connexion `ServiceConnection` (enregistrée par `AddOmikaDataServiceConnection`).
### WebService DB (base WebService/Business)
| Section | Clé | Valeur effective |
|---|---|---|
| `Hfsql` (Business / WebService) | `DatabaseName` | `BATIK_TEST` (config locale) |
La base WebService/Business est utilisée par **Omika.Api** et **Omika.Worker**
via la connexion `Hfsql` (enregistrée par `AddOmikaData`). Elle héberge les
tables `MobReception`, `MobLog` et les données métier (Intervention).
## 4. Confirmation explicite TEST
Le nom de la base Service (`BATIK.01_BLS`) **ne prouve pas** qu'elle est une
base TEST. La confirmation est fournie **explicitement** via la configuration
locale non versionnée `config/hfsql-test.local.json`, section `E2E` :
```json
"E2E": {
"AllowHfSqlWrites": false,
"ServiceDatabaseConfirmedTest": false,
"WebServiceDatabaseConfirmedTest": false
}
```
Par défaut, tous les flags sont `false`. **Aucune écriture** n'est possible tant
que les trois flags ne sont pas explicitement `true` :
- `AllowHfSqlWrites = true`
- `ServiceDatabaseConfirmedTest = true`
- `WebServiceDatabaseConfirmedTest = true`
Le mécanisme est implémenté dans `E2EOptions.IsWriteAllowed` (exige les trois
flags simultanément). Le nom de la base ou `localhost` ne suffit jamais.
## 5. Connexions effectives
### Omika.Api
- `ServiceConnection` → base Service (`BATIK.01_BLS` actuelle).
- `WebServiceConnection` (via `Hfsql`) → base WebService/Business
(`BATIK_TEST` / `OMIKA_TEST` selon l'environnement choisi).
### Omika.Worker
- `ServiceConnection` → même base Service.
- `WebServiceConnection` (via `Hfsql`) → même base WebService/Business.
Aucun mot de passe n'est loggé.
## 6. Probes lecture (avant toute écriture)
Avant toute écriture, les probes de lecture suivantes doivent réussir :
- **ServiceConnection** : ouvrir, `SELECT` lecture sur `TOSRV`.
- **WebServiceConnection** : ouvrir, `SELECT` lecture sur `MobReception`,
`SELECT` lecture sur `MobLog`, `SELECT` lecture sur les tables Intervention
nécessaires.
Aucun `INSERT`, `UPDATE` ou `DELETE`. Si un probe échoue : **STOP**.
## 7. Licence — reste STOP
`BASE_LICENCES` reste physiquement bloquée : les tables `ClientBase`, `Licence`,
`LicenceHisto` sont absentes. Rien n'est modifié dans la base Licence. Le
présent E2E ne valide **pas** le parcours Licence.
## 8. Authentification du test Sync
L'endpoint Sync reste protégé. `MobileAuthMiddleware` n'est **pas désactivé**.
Le test de pipeline Sync utilise un mécanisme explicite de TEST basé sur le
vrai `ITokenService` / `TokenService` :
- `SigningKey` de TEST (configuration locale non versionnée).
- Identité `MobileTokenIdentity` de TEST.
- Bearer réellement validé par `MobileAuthMiddleware`.
Le token est généré par le harness/test E2E (helper `CreateValidToken` dans
`tests/Omika.Tests/E2E/SyncPipelineE2ETests.cs`).
**Limite documentée** : cela valide `Bearer → middleware → endpoint Sync`, mais
**ne valide pas** `Licence → activate → token`.
Aucun bypass auth, aucun middleware désactivé, aucun token hardcodé en source,
aucun token loggé.
## 9. Test E2E automatisé SKIPPABLE
Un test E2E automatisé a été créé : `tests/Omika.Tests/E2E/SyncPipelineE2ETests.cs`.
Il est **SKIPPABLE** : il n'écrit JAMAIS par défaut. Il exige explicitement les
trois garde-fous (`AllowHfSqlWrites`, `ServiceDatabaseConfirmedTest`,
`WebServiceDatabaseConfirmedTest`) tous `true`. Sinon, le test est SKIP.
Le test couvre :
- **CREATE** : envoi via `POST /api/sync/operations` avec un Bearer valide.
- **TOSRV** : vérification de la réception dans la base Service.
- **MobReception** : transfert par le Worker.
- **Business** : création de l'Intervention.
- **MobLog** : résultat terminal SUCCESS.
- **status API** : interrogation de l'endpoint moderne avec OperationId.
- **replay** : réenvoi du même OperationId → idempotence terminale.
## 10. Pipeline validé (une fois les garde-fous satisfaits)
### CREATE
- `OperationId` = nouveau Guid.
- `RequestedEntityGuid` = nouveau Guid.
- Référence/titre : `E2E_TEST_<identifiant court>`.
- Aucune donnée personnelle réelle.
### Envoi normal
`POST /api/sync/operations` avec `Authorization: Bearer <token test valide>`.
Aucun INSERT manuel dans TOSRV pour le test principal. Le pipeline validé est :
HTTP → auth → SyncOperationService → repository → TOSRV.
### TOSRV Service
Après réception, vérifier dans `ServiceConnection.TOSRV` : `OperationId`,
`EntityType`, `OperationType`, `RequestedEntityGuid`. Aucune ligne modifiée
manuellement.
### Worker
Un seul `Omika.Worker` avec les mêmes configurations Service/WebService.
Pipeline : `SyncWorker` → `SyncProcessor` → `TransferToReceptionAsync`.
### Transfert inter-base
`Service.TOSRV` → `WebService.MobReception` → `Service.TOSRV` finalisé/supprimé
selon le comportement actuel. Vérifier surtout que `OperationId` est identique.
### Handler
`MobReception` → `SyncProcessor` → `InterventionSyncOperationHandler` (et non
`DefaultSyncOperationHandler`).
### Business result
Vérifier dans la base métier TEST que l'Intervention est créée. Ne valider que
les propriétés réellement envoyées. Identifier `RequestedEntityGuid` et
`CanonicalEntityGuid` selon le modèle Batik/Omika réellement sélectionné.
### MobLog
Valider dans `WebServiceConnection.MobLog` : `OperationId`, `SUCCESS`,
`CanonicalEntityGuid`, résultat terminal, absence d'erreur en succès.
### Status API
Interroger l'endpoint moderne de status avec `OperationId`. Attendu : `SUCCESS`
correspondant à MobLog. Le test n'est pas complet tant que ce retour API n'est
pas validé.
### Replay
Réenvoyer exactement le même `OperationId` et le même payload. Attendu :
- aucune deuxième Intervention métier ;
- résultat terminal réutilisé ;
- status toujours cohérent.
Documenter précisément **idempotence terminale** et ne pas prétendre
**exactly-once concurrent global**.
### UPDATE
Après CREATE vert, nouvel `OperationId`, tester UPDATE sur l'Intervention E2E.
Pipeline : API → Service.TOSRV → WebService.MobReception → Handler → Business →
MobLog → status SUCCESS.
### ARCHIVE
Après UPDATE vert, nouvel `OperationId`, tester ARCHIVE logique. Sert aussi de
cleanup fonctionnel de l'Intervention E2E. Aucun DELETE physique.
### REJECTED (éventuel)
Si CREATE/UPDATE/ARCHIVE sont verts, envoyer éventuellement un payload invalide
contrôlé. Attendu : `REJECTED` → MobLog → API status `REJECTED`. Ne provoque pas
une erreur d'intégrité dangereuse.
## 11. Cross-database failure
Ne provoque pas volontairement un crash sur les vraies bases. Le scénario
`MobReception insérée + TOSRV encore présente` est déjà couvert par tests fakes.
Pour l'E2E réel, observer uniquement le comportement normal.
## 12. Un seul Worker
Règle actuelle : **UN SEUL `Omika.Worker` actif**. Ne teste pas la concurrence
multi-worker.
## 13. SyncConnection obsolète
Ne supprime pas `SyncConnection` pendant ce test. Ce nettoyage reste séparé.
## 14. Critères pour autoriser l'écriture
Écriture E2E permise UNIQUEMENT si :
- A. `ServiceDatabaseConfirmedTest = true`
- B. `WebServiceDatabaseConfirmedTest = true`
- C. `AllowHfSqlWrites = true`
- D. connexion Service réussie
- E. connexion WebService réussie
- F. `TOSRV` présente dans Service
- G. `MobReception` et `MobLog` présentes dans WebService
- H. base métier cible explicitement TEST
Si un seul manque : **STOP avant écriture**.
## 15. Aucun schéma HFSQL modifié
**Aucun schéma HFSQL n'a été modifié** : aucune création/suppression de table,
aucune modification de colonne. Des **écritures de données** ont été effectuées
dans les bases TEST (TOSRV, MobReception, MobLog) lors de l'exécution E2E, mais
le schéma physique est intact.
## 16. Baseline
- Tests Mobile : **515/515** (référence 511/511 + 4 tests transactionnels ajoutés
par le micro-lot idempotence retry, confirmé).
- Suite unitaire hors HFSQL environnemental : **1755/1755** (référence
1751/1751 + 4 tests transactionnels, confirmé).
- Build solution : **VERT** (confirmé).
- Worker : **VERT**.
- Android : **VERT**.
### Niveau de validation réellement obtenu (compte rendu final)
Le test `SyncPipelineE2ETests.Create_Intervention_Pipeline_IsGreen` a été
**réellement exécuté** (EXECUTED) avec les trois garde-fous à `true` et un
`Omika.Worker` externe actif. Le pipeline a **réellement abouti** : l'Intervention
`E2E_TEST_5b915439` a été créée dans BATIK_TEST, le MobLog a enregistré
`Success`, et l'endpoint status a retourné `SUCCESS`.
**Classification : CAS A — CREATE E2E réellement validé (SUCCESS).**
Le test a réellement traversé : HTTP API → Bearer réel → MobileAuthMiddleware →
POST /api/sync/operations → ServiceConnection.TOSRV → Worker externe →
MobReception → SyncProcessor → InterventionSyncOperationHandler → Business
BATIK_TEST → MobLog → status API SUCCESS.
**Ce qui est réellement validé par ce test dans l'état actuel :**
- HTTP API réelle (via `WebApplicationFactory<Program>`).
- Bearer réel via le vrai `TokenService` → `MobileAuthMiddleware` réellement
traversé.
- `SyncOperationService` → repository → TOSRV (base Service `BATIK.01_BLS`).
- Worker externe → `MobReception` (base WebService `BATIK_TEST`).
- `SyncProcessor` → `InterventionSyncOperationHandler` (handler réellement
sélectionné).
- Intervention créée dans BATIK_TEST (Business réel).
- MobLog `Success` écrit.
- Status API `SUCCESS`.
**Replay** : le test `Replay_SameOperationId_IsIdempotent` a été **réellement
exécuté** (EXECUTED) mais a **échoué** : le status est resté `Pending` au moment
de la vérification (le Worker externe n'a pas traité l'opération dans le délai
du test). Le Worker a néanmoins traité l'opération Replay **après** le test,
créant l'Intervention `E2E_TEST_0fce297f`.
**Diagnostic exact (micro-lot idempotence) :** le test Replay **actuel** génère
son **propre** OperationId (`0fce297f-e4e4-45ad-9027-b4e6e23d5c8a`), distinct de
celui du CREATE (`5b915439`). `E2E_TEST_0fce297f` est donc l'Intervention
**normale** du test Replay, **PAS un doublon** du CREATE. Le test Replay actuel
est **conceptuellement incorrect** : il envoie les deux POST sans attendre le
SUCCESS terminal du premier. Il a été **corrigé** (autonome, polling terminal,
comptage métier = 1). L'idempotence terminale n'est donc **pas encore démontrée**
par une exécution réelle (garde-fous `false`).
**Micro-lot DÉBLOCAGE STARTUP (appliqué) :**
Trois blocages logiciels de startup ont été corrigés :
1. **Startup API — corrigé.** Dans `src/Omika.Api/Program.cs`, le bloc
`using (var scope = app.Services.CreateScope())` disposait le scope de manière
**synchrone** (`Dispose()`), alors que `HfsqlConnection` n'implémente que
`IAsyncDisposable`. Corrigé en `await using var scope =
app.Services.CreateAsyncScope();` (DisposeAsync propre). Validé par le test
`SyncEndpointsTests` (3/3) qui démarre réellement
`WebApplicationFactory<Program>` : plus aucune `InvalidOperationException
IAsyncDisposable`.
2. **Startup Worker DI — corrigé.** `SyncWorker` et `SyncProcessor` attendent le
POCO `SyncOptions` directement dans leur constructeur, mais seul
`IOptions<SyncOptions>` était enregistré. Corrigé dans
`src/Omika.Worker/Program.cs` en enregistrant le POCO comme singleton résolu
depuis `IOptions<SyncOptions>` (valeurs issues de la configuration, jamais
hardcodées). Le bloc `using (var scope = host.Services.CreateScope())` a aussi
été corrigé en `CreateAsyncScope()`.
3. **Transport Business/WebService ODBC → OLEDB — corrigé.** Le chemin ODBC
(`HfsqlConnection` / `IHfsqlConnection`) était utilisé par la détection de
schéma, le wrapper `WebServiceHfSqlConnection` et les repositories métier
(Intervention). Sur cette machine, le pilote ODBC HFSQL est absent
(`IM002 Source de données introuvable`). Un override ciblé OLE DB a été ajouté
(`BusinessOleDbConnection` + `AddOmikaDataBusinessOleDb`) et branché dans
`Program.cs` de l'API et du Worker. La connexion métier passe désormais par
OLE DB `PCSOFT.HFSQL`, sans driver ODBC.
**Startup Worker — blocage ODBC résolu, cycle réel exercé :**
Après les corrections ci-dessus, le Worker **démarre réellement** avec OLE DB
(plus aucune erreur ODBC) et atteint le cycle de synchronisation réel :
```
info: Omika.Worker.SyncWorker[0]
Worker de synchronisation démarré.
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
```
Le cycle exécute réellement `SyncProcessor.ProcessAsync` →
`SyncTransferRepository.TransferToReceptionAsync` → requête SQL réelle sur la
base Service (`BATIK.01_BLS`, serveur 127.0.0.1:4900). La connexion OLE DB à la
base Service fonctionne (la requête est envoyée au serveur HFSQL).
**Décalage de schéma `TOSRV` — CORRIGÉ (alignement code ↔ schéma physique) :**
Le cycle échouait sur une erreur de **schéma** de la table `TOSRV` réelle :
```
System.Data.OleDb.OleDbException (0x80040E14): Rubrique TS_PKID inconnue dans
le fichier. Vérifiez que la requête est en phase avec la description des fichiers.
SELECT >>>>TS_PKID<<<<, TS_Date, TS_Mobile, ...
Code erreur : 70208
```
Le code (`SyncTransferRepository`) référençait une colonne `TS_PKID` dans
`TOSRV`, absente de la table réelle. Un **probe OLE DB en lecture seule** sur la
base Service (`BATIK.01_BLS`) a confirmé que la clé primaire réelle de `TOSRV`
est **`IDTOSRV`** (entier). Le code a été aligné sur le schéma physique :
- `TOSRVRow.cs` : propriété `IdToSrv` mappée sur la colonne `IDTOSRV` (type `int`).
- `SyncTransferRepository.cs` : `SELECT`/`DELETE`/`INSERT` utilisent désormais
`IDTOSRV` (plus aucune référence `TS_PKID`).
- `SyncTransferRepositoryTests.cs` : ligne fake `TOSRV` alignée sur `IDTOSRV`.
Le probe a aussi confirmé que `MobReception` (`MR_PKID`, `MR_IDToSRV`, ...) et
`MobLog` (`ML_PKID`, `ML_DateCreat`, ...) sont conformes aux colonnes utilisées
par le code. Le décalage de schéma `TOSRV` est donc **résolu**.
**Ce que le test validerait si les garde-fous étaient `true`, le pilote ODBC
HFSQL disponible ET un Worker externe tournait :**
- HTTP API réelle (via `WebApplicationFactory<Program>`).
- Bearer réel via le vrai `TokenService` → `MobileAuthMiddleware` réellement
traversé.
- `SyncOperationService` → repository → TOSRV (base Service).
- Worker externe → `MobReception` (base WebService) → handler Intervention →
Business → MobLog → status API.
**Limite restante avant un E2E physique complet :**
1. ~~Corriger le décalage de schéma `TOSRV`~~ — **RÉSOLU** : le code
(`SyncTransferRepository`) et le modèle (`TOSRVRow`) utilisent désormais la
colonne réelle `IDTOSRV` (confirmée par probe OLE DB en lecture seule sur
`BATIK.01_BLS`). Plus aucune référence `TS_PKID`. Le cycle de synchronisation
ne devrait plus échouer sur cette erreur de schéma (code 70208).
2. ~~Passer les trois garde-fous à `true`~~ — **FAIT** (13/08/2026) : le test a
été réellement exécuté avec les garde-fous à `true`.
3. ~~Démarrer un `Omika.Worker` réel~~ — **FAIT** (13/08/2026) : un Worker
externe a été démarré et a traité l'opération jusqu'au handler.
4. ~~Exécuter le test et vérifier l'écriture réelle~~ — **FAIT** (13/08/2026) :
le test CREATE a été exécuté ; TOSRV, MobReception, SyncProcessor, le handler,
le Business, MobLog et le status API ont été réellement exercés. **CREATE
VALIDÉ** : Intervention `E2E_TEST_5b915439` créée, MobLog `Success`, status
API `SUCCESS`.
5. **REPLAY — TEST CORRIGÉ, À RÉ-EXÉCUTER** : le test Replay **actuel** était
conceptuellement incorrect (deux POST sans attendre SUCCESS). Il a été
**corrigé** (autonome, polling terminal, comptage métier = 1). `E2E_TEST_0fce297f`
est l'Intervention **normale** du test Replay (OperationId distinct
`0fce297f`), **PAS un doublon** du CREATE `5b915439`. L'idempotence terminale
n'est **pas encore démontrée** par une exécution réelle (garde-fous `false`).
6. Valider le parcours Licence → activate → token (base Licence toujours
physiquement bloquée).
**Note** : le blocage ODBC (`IM002`) est **résolu** par le routage OLE DB
(`BusinessOleDbConnection`). Le décalage de schéma `TOSRV` (`TS_PKID` → `IDTOSRV`)
est **résolu** par l'alignement code ↔ schéma physique. Le Worker et l'API
démarrent réellement avec OLE DB et atteignent le cycle de synchronisation réel.
Le test E2E CREATE a été **réellement exécuté** (13/08/2026) avec les garde-fous
à `true` et un Worker externe actif, et a **abouti** (SUCCESS). Le test Replay
**actuel** était conceptuellement incorrect (deux POST sans attendre SUCCESS) et
a été **corrigé** (autonome, polling terminal, comptage métier = 1).
`E2E_TEST_0fce297f` est l'Intervention **normale** du test Replay (OperationId
distinct), **PAS un doublon** du CREATE. L'idempotence terminale n'est **pas
encore démontrée** par une exécution réelle (garde-fous `false`).
**Note sur le "faux pass" des tests E2E (garde-fous `false`) :**
Avec les garde-fous `false`, les deux tests `SyncPipelineE2ETests` affichent
"2/2 réussis" mais effectuent un `return` immédiat **avant toute assertion** :
aucun maillon du pipeline n'est exercé. Ce n'est pas un vrai SKIP xUnit (le
framework actuel ne permet pas un skip runtime sans nouvelle dépendance). Il ne
faut donc **pas** interpréter "2/2 E2E verts" comme une validation du pipeline.
Les garde-fous restent `false` : aucune écriture HFSQL n'est possible.
### Réconciliation baseline 1748 → 1742
Le filtre `FullyQualifiedName!~Omika.Tests.Connection.HfSqlOleDb` exclut les
tests dont le FullyQualifiedName contient `Omika.Tests.Connection.HfSqlOleDb`.
**Actuellement, 30 tests sont exclus par ce filtre** (8 classes de tests) :
| Classe de test | Nb tests | Nature |
|---|---|---|
| `HfSqlOleDbConnectionStringBuilderTests` | 11 | unitaires purs (aucun serveur requis) |
| `HfSqlOleDbIntegrationTests` | 4 | intégration réelle (serveur requis) |
| `HfSqlOleDbInterventionIntegrationTests` | 5 | intégration réelle (serveur requis) |
| `HfSqlOleDbCollaborateurLifecycleIntegrationTests` | 2 | intégration réelle (serveur requis) |
| `HfSqlOleDbCreateIntegrationTests` | 2 | intégration réelle (serveur requis) |
| `HfSqlOleDbDossierLifecycleIntegrationTests` | 2 | intégration réelle (serveur requis) |
| `HfSqlOleDbInterventionLifecycleIntegrationTests` | 2 | intégration réelle (serveur requis) |
| `HfSqlOleDbRendezVousLifecycleIntegrationTests` | 2 | intégration réelle (serveur requis) |
**Raison exacte de la baisse 1748 → 1742** : le dernier lot a introduit les
tests `HfSqlOleDb*` (30 tests) qui sont **exclus par le filtre** (leur
FullyQualifiedName contient `Omika.Tests.Connection.HfSqlOleDb`), ainsi que les
tests `E2EOptionsTests` (10 tests visibles). Le nombre de tests "visibles" a
donc diminué de 6 par rapport à la référence 1748, car le dernier lot a déplacé
dans le namespace `HfSqlOleDb` (exclu par le filtre) des tests qui étaient
auparavant comptés dans la suite visible. Le filtre n'a pas changé : il exclut
désormais davantage de tests car davantage de tests portent le préfixe
`HfSqlOleDb` dans leur FullyQualifiedName.
**Note** : le filtre exclut aussi des tests **unitaires purs** (ex.
`HfSqlOleDbConnectionStringBuilderTests`, 11 tests) qui ne nécessitent aucun
serveur HFSQL. Ces tests sont exclus uniquement à cause de leur nom de classe
contenant `HfSqlOleDb`, pas parce qu'ils sont des tests d'intégration. C'est un
effet de bord du filtre par FullyQualifiedName.
## 17. Correction transactionnelle — idempotence retry (implémentée et testée)
La fenêtre de duplication retry (business réussi mais terminal absent) a été
fermée par une **transaction OLE DB locale BATIK_TEST partagée** pour chaque
opération Sync réussie.
### Abstraction retenue
Une abstraction ciblée `IWebServiceTransactionContext` (pas un framework
UnitOfWork générique) expose la session transactionnelle BATIK_TEST :
- `Connection` : la connexion OLE DB BATIK_TEST physique partagée.
- `BeginAsync` / `CommitAsync` / `RollbackAsync`.
`BusinessOleDbConnection` et `WebServiceHfSqlConnection` utilisent cette même
session physique pendant le traitement, garantissant que les commandes métier
(Intervention) et les commandes Sync (MobLog, MobReception) s'exécutent sur la
**même `OleDbConnection`** et la **même `OleDbTransaction`**.
### Support transactionnel HFSQL OLE DB
`HfSqlOleDbConnection` expose désormais `BeginTransaction` / `Commit` /
`Rollback` via `System.Data.OleDb`. Les commandes exécutées pendant une
transaction active reçoivent `command.Transaction = transaction`. Les commandes
hors transaction continuent de fonctionner normalement.
### Orchestration (SyncProcessor)
L'orchestration transactionnelle reste au niveau `SyncProcessor` (le handler
métier reste pur, sans `BeginTransaction`/`Commit`/`Rollback`) :
- **SUCCESS** : `Begin` → handler → MobLog SUCCESS → finalisation MobReception →
`Commit`. Soit tout est durable, soit rien.
- **Retryable failure** (exception ou TransientFailure) : `Rollback` de la
transaction métier, puis retry hors de la transaction annulée (MobReception
reste disponible, retryCount incrémenté). Aucune Intervention partielle durable.
- **PermanentFailure / REJECTED** : `Rollback` des modifications métier, puis
MobLog REJECTED + finalisation MobReception dans une écriture séparée et
durable (le diagnostic REJECTED n'est pas perdu avec le rollback métier).
### TOSRV
TOSRV (`BATIK.01_BLS`) reste **complètement hors** de cette transaction. Aucune
transaction distribuée, aucun DTC, aucun partage de la transaction BATIK_TEST
avec `ServiceConnection`.
### GetTerminalResultAsync
`GetTerminalResultAsync` reste **impérativement** appelé **avant** le handler :
il protège le replay terminal (MobLog SUCCESS existe → handler non rappelé). La
transaction ajoute la protection complémentaire avant terminal.
### Tests ajoutés (SyncProcessorTests)
- `Rollback_On_Exception_Before_MobLog_Leaves_No_Durable_Business` : handler
écrit une Intervention puis lève une exception → ROLLBACK → Intervention
durable = 0, MobLog SUCCESS = 0.
- `Retry_After_Rollback_Commits_Business_And_MobLog` : après rollback, retry →
COMMIT → Intervention = 1, MobLog SUCCESS = 1.
- `Crash_After_Commit_Does_Not_Reapply_Handler_On_Retry` : après COMMIT, replay
→ GetTerminalResultAsync SUCCESS → handler non rappelé, aucune Intervention
supplémentaire.
- `Two_Different_OperationIds_Produce_Two_Business_Creations` : deux OperationId
distincts → 2 créations métier (la transaction n'est pas une déduplication
globale).
**État : implémentée et testée (tests transactionnels verts), mais pas encore
validée par un E2E physique (garde-fous `false`).**
## 18. Feu vert pour LOT INTERVENTION MOBILE WRITE
**PARTIEL** — le test end-to-end réel **CREATE** a été **exécuté** (EXECUTED) et
a **abouti** (SUCCESS) : l'Intervention `E2E_TEST_5b915439` a été créée dans
BATIK_TEST, le MobLog a enregistré `Success`, et l'endpoint status a retourné
`SUCCESS`. Le pipeline complet (API → TOSRV → Worker → MobReception → Handler →
Business → MobLog → Status) est **réellement validé** pour CREATE.
Le test **Replay** a été **exécuté** (EXECUTED) mais a **échoué** : le status est
resté `Pending` au moment de la vérification (le Worker externe n'a pas traité
l'opération dans le délai du test). Le Worker a néanmoins traité l'opération
Replay **après** le test, créant l'Intervention `E2E_TEST_0fce297f`.
**Diagnostic exact (micro-lot idempotence) :** le test Replay **actuel** génère
son **propre** OperationId (`0fce297f-e4e4-45ad-9027-b4e6e23d5c8a`), distinct de
celui du CREATE (`5b915439`). `E2E_TEST_0fce297f` est donc l'Intervention
**normale** du test Replay, **PAS un doublon** du CREATE. Le test Replay actuel
est **conceptuellement incorrect** : il envoie les deux POST sans attendre le
SUCCESS terminal du premier. Il a été **corrigé** (autonome, polling terminal,
comptage métier = 1). L'idempotence terminale n'est donc **pas encore démontrée**
par une exécution réelle (garde-fous `false`).
Le feu vert **complet** pour le lot INTERVENTION MOBILE WRITE reste conditionné
à une nouvelle exécution CREATE + Replay réussie avec le test Replay corrigé
(autonome, polling terminal, comptage métier = 1).