# 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_`. - Aucune donnée personnelle réelle. ### Envoi normal `POST /api/sync/operations` avec `Authorization: Bearer `. 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`). - 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` : plus aucune `InvalidOperationException IAsyncDisposable`. 2. **Startup Worker DI — corrigé.** `SyncWorker` et `SyncProcessor` attendent le POCO `SyncOptions` directement dans leur constructeur, mais seul `IOptions` était enregistré. Corrigé dans `src/Omika.Worker/Program.cs` en enregistrant le POCO comme singleton résolu depuis `IOptions` (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`). - 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).