Skip to content

test(e2e): Phase 2 verte — S2/S5/S6/S7 débloqués, fixes #136 couverts, CI E2E fiabilisée - #138

Merged
macminisupport merged 36 commits into
mainfrom
fix/e2e-phase2-green
Sep 7, 2026
Merged

test(e2e): Phase 2 verte — S2/S5/S6/S7 débloqués, fixes #136 couverts, CI E2E fiabilisée#138
macminisupport merged 36 commits into
mainfrom
fix/e2e-phase2-green

Conversation

@kherembourg

@kherembourg kherembourg commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Objectif

Rendre la Phase 2 E2E du SDK Flutter v6 verte et fiable : couverture des fixes Go/No-Go (PR #136), déblocage des scénarios S2/S5/S6/S7, et fiabilisation des jobs CI E2E (hang Android résolu à la racine).

Nouvelles suites (toutes en hard gate CI)

Suite Plateforme Scénario couvert
modal_dismissible_ios_test.dart iOS Fix M1 : modal(dismissible:false) ignore le swipe + couverture du swipe interactif (dismissible:true)
re_display_test.dart / re_display_ios_test.dart Android + iOS Fix M2 : le re-display d'un handle préchargé garde la source (identité re-parsée du natif + cross-check event-stream)
flow_dismiss_test.dart / flow_dismiss_ios_test.dart Android + iOS S2 : display + dismiss d'un Flow (integration_test_flow, flow integration_test_v_1, preuve preload + PLYPresentation.flowId)
interceptor_actions_ios_test.dart iOS S5/S6 : tap réel sur un bouton Navigate, PLYInterceptResult.failed vs .notHandled, outcomes distincts (AppLifecycleState), ordre des callbacks asserté
purchase_restore_ios_test.dart + Configuration.storekit + host RunnerIntegrationTests iOS S7 : achat StoreKit local (.purchased) + restore — voir « Limites »
purchase_restore_android_test.dart Android S7 : dégradation propre prouvée sans Play Store (restore timeout 15 s net, jamais de hang)

Durcissement des suites existantes

  • helpers/e2e_start.dart : retry 3× + backoff sur erreurs réseau/TLS uniquement (flake TLS de start() en CI), adopté par les 22 suites.
  • deeplink_cold_start_test.dart : assertion explicite PRESENTATION_OPENED ABSENT ; ordre DEEPLINK_OPENED/PRESENTATION_LOADED volontairement non contraint (divergence iOS/Android).
  • inline_paywall_test.dart : suite orpheline désormais câblée en CI.

CI (e2e-android.yml, e2e-ios.yml, ci_run_e2e*.sh)

  • Fix racine du hang E2E Android : flutter-version 3.24.x → 3.44.0 (post-AGP 9/chore(android): validate Flutter 3.44 with AGP 9 #130, compileGroovy échouait en 7 s mais integration_test attendait son timeout de 12 min ×3 → job tué à 60 min).
  • Watchdog par tentative (600 s, kill du groupe de process, compatible ubuntu + macos), le retry ×3 couvre aussi les timeouts ; marqueurs ::group::SUITE <name> attempt <n> + code retour + durée pour rendre tout futur hang diagnosticable.
  • Tout est hard gate : le modèle best-effort (warning silencieux) est supprimé. Unique exception, étroite et bruyante : la suite StoreKit n'est non-gating QUE si toutes ses tentatives échouées matchent la signature du bug Apple FB22237318 — tout autre échec gate.
  • Zéro || true / continue-on-error ajouté (et purge de ceux des scripts touchés).
  • 5 wrappers drivers (dont sync log-driven S5→S6 et re-foreground post-Safari pour interceptor_actions).

Preuves locales (sweep complet post-rebase)

  • Android : 12/12 PASS, gate fail=0 (5 m 33, tentative 1 partout).
  • iOS : 11/14 PASS (12 m 21) ; storekit → marqueur explicite S7-iOS BLOCKED (Apple FB22237318) non-gating (comportement conçu) ; modal_dismissible (test dismissible:true) FAIL 3/3 local → arbitrage CI (sim iOS 26.5 local suspect, couche Flutter innocentée par revue de la sérialisation) ; interceptor_actions → bug driver corrigé dans cette PR (ellipse Unicode + $SUITE_LOG non bracé sous set -u).

Limites connues (dites honnêtement)

  • S7 iOS : le .purchased n'a jamais été observé sur la machine locale — bug Apple ouvert (SKTestSession SKInternalErrorDomain Code=3, FB22237318, Xcode 26.5+, reproduit dans [Only on 26.3/26.4][in_app_purchase_storekit] iOS native tests failing with timeouts, SKInternalErrorDomain, and crashes flutter/flutter#184678). La suite est câblée et stricte (aucun échappatoire fake-green) ; le runner CI (Xcode plus ancien) peut ne pas être affecté.
  • S7 Android : achat réel structurellement non testable sur émulateur sans Play Store — couvert par la preuve de dégradation propre.
  • Gaps bridge documentés (candidats follow-ups) : onPresented réassigné sur handle post-preload ignoré ; PLYEventProperties sans champs flow ; fallback _outcomeFromMap sur l'objet caché.

Résultats CI sur le HEAD (6cd504d)

Workflow Run Résultat
E2E iOS 29778281515 14/14 suites — dont storekit-ios PASS tentative 1 : le Xcode du runner n'est pas affecté par FB22237318, S7 iOS est donc réellement prouvé en CI (.purchased + restore). modal-dismissible PASS (arbitrage : le FAIL local était la flakiness sim 26.5). dismiss-ios vert en tentative 2 (retry nominal).
E2E Android 29781770765 12/12 suites, tentative 1, gate fail=0 — après correction d'une cause CI-only : un dialogue ANR du launcher restait au premier plan de l'AVD froide et volait tous les inputs uiautomator (prouvé par IDs de fenêtres identiques inter-suites) ; les drivers force-stoppent désormais le package ANR à chaque poll.
CI (standard) 29780876701

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR delivers Phase 2 of the Flutter SDK v6 E2E suite by unlocking scenarios S2/S5/S6/S7, hardening the CI runners (watchdog, all-hard-gate model, Flutter 3.44.0 to fix the AGP9 hang), and adding six new test suites with their host-side drivers. It resolves the root cause of the Android E2E job hang and adds a StoreKitTest-backed XCUITest target for real local StoreKit2 purchase+restore coverage.

  • New suites (modal_dismissible, re_display, flow_dismiss, interceptor_actions, purchase_restore on both platforms): all wired as hard gates, backed by startWithRetry (network-error-only retry) and the new per-attempt 600 s watchdog.
  • CI hardening: run_with_timeout (portable watchdog, process-group kill), --reporter expanded unified across both platforms, ANR-clearing in Android drivers (clear_stuck_anr), and the StoreKit non-gating path scoped strictly to the documented Apple bug signature (FB22237318).
  • Dart result-marker gate for the StoreKit suite: run_storekit_suite_ios.sh gates on S7-IOS-RESULT: PASS in the simulator unified log, not on xcodebuild's exit code alone, preventing false-green from xcodebuild exiting 0 while Dart assertions silently fail.

Confidence Score: 5/5

Safe to merge — the changes are test infrastructure only (no production code path is touched) and CI results confirm 12/12 Android and 14/14 iOS suites green on the head commit.

All changed files are E2E test code, CI scripts, and Xcode test targets. The watchdog, hard-gate promotion, StoreKit Dart-marker gate, and ANR-clearing mechanisms are well-reasoned and backed by observed CI runs. The one noteworthy finding — misleading manual run instructions in purchase_restore_ios_test.dart — affects only developers running the test by hand, not CI correctness.

purchase_restore_ios_test.dart header comment (manual run instructions reference a background idb driver that no longer applies now that RunnerIntegrationTests.m handles the CTA tap internally).

Important Files Changed

Filename Overview
purchasely/example/integration_test/tools/ci_run_e2e.sh Rewritten to all-hard-gate model with portable run_with_timeout watchdog, --reporter expanded, ANR-clear preamble, and 4 new suites (inline_paywall, re_display, flow_dismiss, purchase_restore_android). Logic is sound.
purchasely/example/integration_test/tools/ci_run_e2e_ios.sh iOS runner extended to 14 suites with same watchdog/hard-gate model; StoreKit Apple-bug exception logic (storekit_apple_sig vs storekit_other_failure) is correctly scoped to all-Apple-only failure runs.
purchasely/example/integration_test/tools/run_storekit_suite_ios.sh Dart-marker-authoritative gate correctly requires both xcodebuild exit 0 AND S7-IOS-RESULT: PASS in the simulator unified log; log show fallback handles log stream buffering races.
purchasely/example/integration_test/helpers/e2e_start.dart startWithRetry correctly gates retries on network-ish error substrings only; 180 s backstop timeout deliberately not classified as retryable to surface genuine hangs.
purchasely/example/integration_test/purchase_restore_ios_test.dart S7-IOS-RESULT marker mechanism correctly uses _completedTests++ as last line of test body; manual run instructions in header still reference tap_purchase_ios.sh alongside xcodebuild, which silently fails now that RunnerIntegrationTests.m owns the CTA tap.
purchasely/example/ios/RunnerIntegrationTests/RunnerIntegrationTests.m SKTestSession setup, CTA tap via XCUITest, and 420 s poll with XCTFail are correctly wired; disableDialogs eliminates SpringBoard confirmation sheet dependency.
purchasely/example/integration_test/interceptor_actions_ios_test.dart S5/S6 correctly distinguish failed vs notHandled via AppLifecycleState transitions; load-proof assertion on presentation.type prevents silent fallback to bundled screen.
purchasely/example/integration_test/purchase_restore_android_test.dart Honest-degradation suite: proves interceptor fires, then asserts restore fast-fails (≤20 s) without a real Play Store; all three outcome paths (resolved/timeout/PlatformException) are caught and asserted.
.github/workflows/e2e-android.yml Flutter 3.44.0 pin fixes the AGP9/compileGroovy hang; step-level 50-min ceiling preserves log upload even on worst-case retry exhaustion.
.github/workflows/e2e-ios.yml Mirrors Android workflow with 50-min step ceiling; branch trigger extended to feat/sdk-v6-migration to gate the current migration branch.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CI as GitHub Actions
    participant Script as ci_run_e2e_ios.sh
    participant RWT as run_with_timeout
    participant FT as flutter test
    participant Driver as Driver script
    participant Sim as iOS Simulator

    CI->>Script: bash ci_run_e2e_ios.sh udid
    loop Each suite (1-13)
        Script->>Driver: "bash driver.sh udid &"
        Script->>RWT: run_with_timeout flutter test
        RWT->>FT: flutter test (set -m own PGID)
        FT->>Sim: install + launch app
        Driver->>Sim: idb tap / xcrun simctl launch
        Sim-->>FT: Dart callbacks / events
        FT-->>RWT: exit 0 or non-zero
        alt timeout (600 s)
            RWT->>FT: kill -TERM PGID then kill -KILL PGID
            RWT-->>Script: exit 124
        else pass
            RWT-->>Script: exit 0
        end
        Script->>Driver: kill driver PID
    end

    Note over Script: Suite 14 StoreKit special path
    Script->>RWT: run_with_timeout bash run_storekit_suite_ios.sh udid
    RWT->>Script: xcodebuild test RunnerIntegrationTests
    Script->>Sim: xcrun simctl spawn log stream flutter
    Sim-->>Script: S7-IOS-RESULT PASS or FAIL
    alt xcodebuild exit 0 AND PASS marker
        Script-->>CI: exit 0
    else Apple FB22237318 signature only
        Script-->>CI: warning S7-iOS BLOCKED non-gating
    else other failure
        Script-->>CI: exit 1 gate
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CI as GitHub Actions
    participant Script as ci_run_e2e_ios.sh
    participant RWT as run_with_timeout
    participant FT as flutter test
    participant Driver as Driver script
    participant Sim as iOS Simulator

    CI->>Script: bash ci_run_e2e_ios.sh udid
    loop Each suite (1-13)
        Script->>Driver: "bash driver.sh udid &"
        Script->>RWT: run_with_timeout flutter test
        RWT->>FT: flutter test (set -m own PGID)
        FT->>Sim: install + launch app
        Driver->>Sim: idb tap / xcrun simctl launch
        Sim-->>FT: Dart callbacks / events
        FT-->>RWT: exit 0 or non-zero
        alt timeout (600 s)
            RWT->>FT: kill -TERM PGID then kill -KILL PGID
            RWT-->>Script: exit 124
        else pass
            RWT-->>Script: exit 0
        end
        Script->>Driver: kill driver PID
    end

    Note over Script: Suite 14 StoreKit special path
    Script->>RWT: run_with_timeout bash run_storekit_suite_ios.sh udid
    RWT->>Script: xcodebuild test RunnerIntegrationTests
    Script->>Sim: xcrun simctl spawn log stream flutter
    Sim-->>Script: S7-IOS-RESULT PASS or FAIL
    alt xcodebuild exit 0 AND PASS marker
        Script-->>CI: exit 0
    else Apple FB22237318 signature only
        Script-->>CI: warning S7-iOS BLOCKED non-gating
    else other failure
        Script-->>CI: exit 1 gate
    end
Loading

Reviews (2): Last reviewed commit: "fix(e2e): make iOS review gates reliable" | Re-trigger Greptile

Comment thread purchasely/example/integration_test/tools/run_storekit_suite_ios.sh Outdated
Comment thread purchasely/example/integration_test/tools/swipe_dismiss_ios.sh Outdated
Comment thread purchasely/example/integration_test/tools/ci_run_e2e.sh Outdated
Comment thread purchasely/example/integration_test/helpers/e2e_start.dart

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands and hardens the Flutter SDK v6 E2E test coverage (new Phase 2 suites for S2/S5/S6/S7 and regression guards for fixes from #136) while making CI E2E runs more reliable and diagnosable (portable per-attempt watchdog, retries, and stricter hard-gating).

Changes:

  • Added new Android+iOS E2E suites for flow dismiss (S2), action interceptor outcomes (S5/S6), re-display source preservation (M2), modal swipe dismiss behavior (M1), and purchase/restore coverage (S7).
  • Introduced shared startWithRetry() to reduce CI flakes from transient network/TLS failures during Purchasely.start().
  • Reworked CI E2E runners/scripts to be hard-gated with per-attempt watchdogs and improved diagnostics; updated workflows (notably Flutter 3.44.0 for AGP9 compatibility on Android).

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
purchasely/example/ios/RunnerIntegrationTests/RunnerIntegrationTests.m Adds a host UI test bundle to launch the app under SKTestSession for StoreKit E2E.
purchasely/example/ios/RunnerIntegrationTests/Info.plist Adds Info.plist for the new RunnerIntegrationTests target.
purchasely/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme Wires RunnerIntegrationTests into the scheme and attaches the StoreKit configuration reference.
purchasely/example/ios/Runner.xcodeproj/project.pbxproj Adds the RunnerIntegrationTests target and includes Configuration.storekit as a resource.
purchasely/example/ios/Configuration.storekit Defines local StoreKit products for S7 iOS purchase/restore testing.
purchasely/example/integration_test/helpers/e2e_start.dart Adds startWithRetry() helper used by multiple suites.
purchasely/example/integration_test/user_attribute_listener_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/local_dismiss_handler_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/local_dismiss_handler_ios_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/interceptor_trigger_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/interceptor_trigger_ios_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/inline_paywall_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/inline_events_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/default_dismiss_handler_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/default_dismiss_handler_ios_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/default_dismiss_via_display_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/default_dismiss_via_display_ios_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/deeplink_cold_start_test.dart Strengthens event assertions and switches SDK start to startWithRetry().
purchasely/example/integration_test/dart_android_bridge_test.dart Switches SDK start to startWithRetry().
purchasely/example/integration_test/dart_ios_bridge_test.dart Updates comments + switches SDK start to startWithRetry().
purchasely/example/integration_test/re_display_test.dart New Android suite guarding M2 (re-display preserves original source).
purchasely/example/integration_test/re_display_ios_test.dart New iOS mirror suite guarding M2 (re-display preserves original source).
purchasely/example/integration_test/flow_dismiss_test.dart New Android suite for S2 (flow display + dismiss).
purchasely/example/integration_test/flow_dismiss_ios_test.dart New iOS mirror suite for S2 (flow display + dismiss).
purchasely/example/integration_test/modal_dismissible_ios_test.dart New iOS suite guarding M1 and exercising swipe-dismiss for dismissible modals.
purchasely/example/integration_test/interceptor_actions_ios_test.dart New iOS suite for S5/S6 (navigate action interceptor failed vs notHandled).
purchasely/example/integration_test/purchase_restore_ios_test.dart New iOS suite for S7 (local StoreKit purchase + restore).
purchasely/example/integration_test/purchase_restore_android_test.dart New Android “honest degradation” S7 suite (no Play Store).
purchasely/example/integration_test/tools/ci_run_e2e.sh Makes all Android suites hard-gated; adds watchdog and improved diagnostics.
purchasely/example/integration_test/tools/ci_run_e2e_ios.sh Makes all iOS suites hard-gated with StoreKit Apple-bug exception logic + watchdog.
purchasely/example/integration_test/tools/run_storekit_suite_ios.sh New StoreKit iOS runner (xcodebuild + simulator log capture).
purchasely/example/integration_test/tools/tap_purchase.sh Adds ANR-dialog recovery to reduce CI flakiness.
purchasely/example/integration_test/tools/press_back.sh Adds ANR-dialog recovery to reduce CI flakiness.
purchasely/example/integration_test/tools/tap_content_desc.sh New generalized Android UI driver for content-desc substring taps.
purchasely/example/integration_test/tools/flow_close_all.sh New wrapper driver to tap action:close_all for flow dismiss on Android.
purchasely/example/integration_test/tools/re_display_driver.sh New Android driver wrapper to close paywall twice for re-display suite.
purchasely/example/integration_test/tools/re_display_driver_ios.sh New iOS driver wrapper to close paywall twice for re-display suite.
purchasely/example/integration_test/tools/tap_label_ios.sh New generalized iOS idb driver to tap elements by AXLabel.
purchasely/example/integration_test/tools/swipe_dismiss_ios.sh New iOS driver to send a fixed number of interactive swipe-down gestures.
purchasely/example/integration_test/tools/modal_dismissible_driver_ios.sh New iOS driver wrapper to run swipe-dismiss twice for modal dismissible suite.
purchasely/example/integration_test/tools/interceptor_actions_driver_ios.sh New iOS driver coordinating two “Login” taps with log-driven synchronization + re-foreground.
.github/workflows/e2e-android.yml Updates Flutter version to 3.44.0 and adds step-level timeout.
.github/workflows/e2e-ios.yml Adds step-level timeout to preserve diagnostics on failures/hangs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread purchasely/example/ios/RunnerIntegrationTests/RunnerIntegrationTests.m Outdated
Comment thread purchasely/example/ios/Runner.xcodeproj/project.pbxproj
Comment thread purchasely/example/integration_test/tools/run_storekit_suite_ios.sh Outdated
kherembourg added a commit that referenced this pull request Jul 21, 2026
…start backstop timeout

Three cheap Greptile P2 findings from PR #138:

- swipe_dismiss_ios.sh: the no-frame fallback geometry (390x844, iPhone-SE
  proportions) undershoots the swipe endpoints by ~1% on an iPhone 15 Pro
  simulator (393x852), making dismissal less reliable. Bump to 390 852.
- ci_run_e2e.sh (Android): add --reporter expanded to match the iOS runner
  (ci_run_e2e_ios.sh already has it), so a failing suite's log artifact
  shows per-test detail instead of a single "Some tests failed" line.
- e2e_start.dart: add a 180s backstop `.timeout(...)` around each
  startWithRetry attempt so callers without their own Dart-level timeout
  (e.g. flow_dismiss_test.dart) can't hang for the full ~600s CI watchdog
  budget. A caller's own tighter timeout still wins. Left as the default
  TimeoutException (no "timed out" in its message) so it does NOT match
  _kNetworkNeedles and get silently retried like a network hiccup — a real
  hang should surface as a failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macminisupport

Copy link
Copy Markdown
Contributor

@greptileai review

Base automatically changed from feat/sdk-v6-migration to main July 21, 2026 16:06
kherembourg added a commit that referenced this pull request Jul 21, 2026
…start backstop timeout

Three cheap Greptile P2 findings from PR #138:

- swipe_dismiss_ios.sh: the no-frame fallback geometry (390x844, iPhone-SE
  proportions) undershoots the swipe endpoints by ~1% on an iPhone 15 Pro
  simulator (393x852), making dismissal less reliable. Bump to 390 852.
- ci_run_e2e.sh (Android): add --reporter expanded to match the iOS runner
  (ci_run_e2e_ios.sh already has it), so a failing suite's log artifact
  shows per-test detail instead of a single "Some tests failed" line.
- e2e_start.dart: add a 180s backstop `.timeout(...)` around each
  startWithRetry attempt so callers without their own Dart-level timeout
  (e.g. flow_dismiss_test.dart) can't hang for the full ~600s CI watchdog
  budget. A caller's own tighter timeout still wins. Left as the default
  TimeoutException (no "timed out" in its message) so it does NOT match
  _kNetworkNeedles and get silently retried like a network hiccup — a real
  hang should surface as a failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kherembourg
kherembourg force-pushed the fix/e2e-phase2-green branch from 2d1e807 to 951362d Compare July 21, 2026 16:10
@kherembourg

Copy link
Copy Markdown
Collaborator Author

Review avant merge — à merger, avec une réserve

Contenu vérifié et toujours pertinent malgré les 6 semaines : 0 commit de retard sur main, 11/11 verte, et le fond est réel — suites 4→8 promues de best-effort à HARD gate (les drivers UI ont donc été fiabilisés), 4 nouvelles suites (inline render, re-display source PR #136 M2, Flow S2, purchase/restore S7), 14 fichiers de test ajoutés, un helpers/e2e_start.dart, une Configuration.storekit et une cible RunnerIntegrationTests.

⚠️ Une régression à ne pas embarquer : la suppression du trigger pull_request

Cette PR retire on: pull_request de e2e-android.yml et e2e-ios.yml, laissant workflow_dispatch + nightly.

La justification d'origine se tient pour l'époque (« while the v6 migration is in flight »), mais l'expérience depuis dit le contraire. Sur #153, l'E2E déclenchée sur PR a attrapé deux choses qu'aucun test unitaire ne voyait :

  • un échec déterministe 3/3 dû à appHandlesRedemptionAlert non passé à true — invisible en local, invisible en unitaire ;
  • la preuve que proxy(null) et anonymousUserId fonctionnent réellement contre le backend.

Sans le trigger PR, ces deux-là ne se voient qu'après le merge, sur le nightly. Je propose de garder on: pull_request sur les deux workflows lors du rebase. Le passage du cron à 05:00 UTC et tout le reste, je le garde tel quel.

Conflit attendu, et une redondance amusante

3 fichiers en collision avec #153 : e2e-android.yml, ci_run_e2e.sh, ci_run_e2e_ios.sh.

Sur e2e-android.yml la collision est une redondance : cette PR contient déjà le bump 3.24.x → 3.44.0 avec exactement le même diagnostic que celui que j'ai posé dans #153 (flutter.groovy importe groovy.xml.QName, que Gradle 9 n'expose plus ; échec masqué par le timeout de 12 min de flutter_tools ; « le seul workflow que #130 a oublié »). J'ai rediagnostiqué de zéro un fix qui dormait ici depuis six semaines — l'E2E Android était rouge chaque nuit sur main pendant tout ce temps alors que le correctif existait déjà.

Sur les deux ci_run_e2e*.sh c'est une vraie collision de numérotation : cette PR va à 12 suites, #153 à 10. Je rebase et je renumérote à 14 après le merge de #153.

Plan : #153 d'abord (release, quasi verte), puis rebase de celle-ci avec le trigger pull_request conservé et les suites renumérotées, revérification CI, puis merge.

kherembourg and others added 18 commits September 7, 2026 18:27
…ent assertion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…5/S6)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uites as hard gates

Root fix: e2e-android.yml pinned flutter-version 3.24.x post-AGP9 (#130),
breaking compileGroovy under Gradle 9 and surfacing only via
flutter_tools' 12-minute per-test timeout, x3 retries, burning the whole
60-minute job on the first 1-2 suites. Bumped to 3.44.0 (e2e-ios.yml's
3.41.4 was already correct, left untouched).

ci_run_e2e.sh / ci_run_e2e_ios.sh:
- Portable per-attempt watchdog (run_with_timeout, no GNU coreutils
  dependency) wrapping every flutter test/xcodebuild invocation; two real
  bugs found and fixed via local testing (not just static review): an
  orphaned watchdog child holding a piped tee open (stalling every
  attempt for the full timeout), and an empty marker file defeating the
  124 timeout-exit-code detection.
- ::group::SUITE <name> attempt <n> / ::endgroup:: markers with exit
  code + duration on every attempt.
- All suites promoted from best-effort/::warning:: to HARD gates (no
  || true, no continue-on-error) except one explicit, narrowly-scoped
  exception: the iOS StoreKit suite prints a loud "S7-iOS BLOCKED
  (Apple FB22237318)" marker and does not gate only when its failure
  matches that known, currently-open Apple/Xcode platform bug signature;
  any other failure of that suite gates normally.
- Wired inline_paywall_test.dart (previously orphaned) plus the new
  re_display, flow_dismiss, purchase_restore_android/ios,
  modal_dismissible_ios and interceptor_actions_ios suites, adding 5 new
  driver-wrapper scripts (re_display_driver[_ios],
  modal_dismissible_driver_ios, flow_close_all, and a log-driven
  two-tap + re-foreground driver for interceptor_actions_ios).

Local proof on emulator-5554: 2 suites passed end-to-end through the
fixed script (user_attribute_listener, re_display — the latter also
proving the new chained press_back.sh driver), plus a TIMEOUT=5 run
showing 3 clean kill+retry cycles ending in a correctly gated failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…exception

Fix 1: run_with_timeout backgrounds the suite command with job control
(set -m) so it gets its own process group, then kills the group on timeout
instead of just $cmd_pid — reaches gradle/dart/xcodebuild children that a
single-PID kill orphaned. Portable: process-group creation via job control
is core POSIX shell behavior, not a coreutils extra like setsid/timeout, so
no per-runner branching is needed. Verified on the real emulator: 3 timed-
out attempts leave zero leaked flutter/dart/gradle-client processes behind
(only the pre-existing shared gradle daemon remains, as expected), and the
normal/green path still completes correctly afterward.

Fix 2: the StoreKit (FB22237318) non-gating exception now requires every
failed attempt to match the Apple bug signature, not just any one of them —
a mixed run (one attempt hits the known bug, another fails for a real
regression) now gates instead of silently passing. Banner text states
exactly how many attempts matched instead of a hardcoded "all 3 attempts".

Fix 3: removed the newly-added || true instances (interceptor re-foreground,
storekit cleanup) plus run_suite()'s pre-existing ones in both scripts,
replaced with explicit non-fatal logging on failure. grep -c '|| true' on
all three touched files is now 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…set -u

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocus

Task-9 CI run 29778283517: dumpsys window showed the IDENTICAL
mCurrentFocus window IDs (ImmersiveModeConfirmation + "App Not Responding:
com.google.android.apps.nexuslauncher") pinned across 5 different failing
suites — one launcher ANR, under CI's resource-starved cold AVD, holds
input focus for the rest of the job, so every driver BACK/tap lands on
that dialog instead of the paywall. press_back.sh/tap_purchase.sh/
tap_content_desc.sh now force-stop the ANR'd package (never our own app)
every poll iteration; ci_run_e2e.sh also pre-confirms the immersive-mode
dialog to remove one contender. Bridge suite was never affected — it has
no uiautomator driver at all, the T8/T9 comments only point at the
separate interceptor/dismiss suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… P1)

CI run 29778281515 was a confirmed false green: xcodebuild exited 0 while
the Dart suite had actually failed `expect(capturedPayload,
isA<PLYPurchasePayload>())` (interceptor never fired). RunnerIntegrationTests
is a hostless XCTest bundle that only proves the app launched and
exited/timed out, and run_storekit_suite_ios.sh's `exit $STATUS` never
looked at the captured Dart log at all.

purchase_restore_ios_test.dart now bumps a per-test completion counter as
the last line of each test body and prints exactly one
`S7-IOS-RESULT: PASS`/`FAIL (completed=N/M)` marker from tearDownAll.
run_storekit_suite_ios.sh gates its exit code on BOTH xcodebuild exit 0 AND
that PASS marker, hardens the log capture (longer drain + a `log show`
fallback appended after the live stream is killed), and RunnerIntegrationTests.m
now XCTFails if the app is still running after the 180s poll window instead
of silently falling through to a pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…start backstop timeout

Three cheap Greptile P2 findings from PR #138:

- swipe_dismiss_ios.sh: the no-frame fallback geometry (390x844, iPhone-SE
  proportions) undershoots the swipe endpoints by ~1% on an iPhone 15 Pro
  simulator (393x852), making dismissal less reliable. Bump to 390 852.
- ci_run_e2e.sh (Android): add --reporter expanded to match the iOS runner
  (ci_run_e2e_ios.sh already has it), so a failing suite's log artifact
  shows per-test detail instead of a single "Some tests failed" line.
- e2e_start.dart: add a 180s backstop `.timeout(...)` around each
  startWithRetry attempt so callers without their own Dart-level timeout
  (e.g. flow_dismiss_test.dart) can't hang for the full ~600s CI watchdog
  budget. A caller's own tighter timeout still wins. Left as the default
  TimeoutException (no "timed out" in its message) so it does NOT match
  _kNetworkNeedles and get silently retried like a network hiccup — a real
  hang should surface as a failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CI autopsy showed the tap driver legitimately needs ~44s to find the
CTA on a cold runner simulator while the interceptor-fire budget was 40s.
Present 20s->60s, fire 40s->120s, outcome 90s->180s, host poll 180s->420s
(each still bounded well under the 600s per-attempt watchdog).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…K path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kherembourg and others added 17 commits September 7, 2026 18:27
Reconciliation after rebasing onto 6.1.0 (#153). Two things, both deliberate.

## The pull_request trigger stays

This branch dropped `on: pull_request` from e2e-android.yml and e2e-ios.yml,
leaving workflow_dispatch + nightly. The original reasoning holds for its time
("while the v6 migration is in flight"), but experience since says keep it.

On #153 the PR-triggered E2E caught a deterministic 3/3 failure no unit test and
no local run saw: a listener that never fired because `appHandlesRedemptionAlert`
defaults to false, so the SDK waits for a popin nobody acknowledges in CI. It
also produced the only proof that `proxy(null)` and `anonymousUserId` work
against the real backend. Without the trigger, that class of bug surfaces on the
nightly — i.e. after merge.

Everything else from this branch is kept, cron at 05:00 UTC included.

## #153's two suites, renumbered per runner

The rebase kept this branch's version of both runner scripts, so #153's suites
were dropped from them while their test files stayed (they came from main). Added
back, respecting each runner's own structure:

- Android: `Suite 13/14` and `Suite 14/14`, after this branch's 12.
- iOS: `Batch 8/9` and `Batch 9/9`. This branch restructured the iOS runner from
  12 individual suites into 7 consolidated batches plus an xcodebuild-driven
  StoreKit batch, so a naive renumber would have been wrong — these two stay
  standalone rather than folded into a batch, because each starts the SDK with a
  distinct proxy/identity state and `start()` only runs once per process.

Verified: 360 Dart tests, analyze clean, `dart format` clean under Dart 3.5.4,
both workflow files parse, both runner scripts pass `bash -n`, and #153's
`minSdk = 23` pin survived the rebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The iOS E2E job failed on 'Batch 8/9: 6.1.0 identity + cleared proxy + redemption
listener', but the suite log holds only 'loading' and the build warnings — the app
never launched. That is the known simulator launch flake, which the retry loop is
designed to absorb; Batch 1 hit the same 600s timeout on this run and passed on
attempt 2.

What actually failed is the wall clock. Batches 1-7 took 23 minutes including one
retry, Batch 8's first attempt burned its full 600s, and the job's 60-minute cap
cut attempt 2 before it finished. Batch 9 never ran. So a passing suite looked
like a failing one.

This branch consolidated 12 individual iOS suites into 7 batches precisely to fit
60 minutes; #153 then added two more. 90 gives the retry loop room to do its job
instead of being truncated by the cap.

The same cap is raised on Android for symmetry: it now runs 14 suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kherembourg

Copy link
Copy Markdown
Collaborator Author

Diagnostic du check rouge — ce n'était pas une suite qui échoue

E2E iOS a rapporté un échec sur Batch 8/9: 6.1.0 identity + cleared proxy + redemption listener. La log de suite téléchargée depuis l'artefact ne contient que :

00:00 +0: loading .../redemption_identity_test.dart
Warning: Missing build name (CFBundleShortVersionString).

Puis rien pendant 600 s. L'app n'a jamais démarré — pas un setUpAll qui échoue, pas une assertion qui casse. C'est le flake de lancement sur simulateur déjà documenté, et le retry est là pour l'absorber : Batch 1 a pris exactement le même timeout de 600 s sur ce run et est passé à l'attempt 2 en 183 s.

Ce qui a réellement échoué, c'est le mur de temps :

Batches 1–7 16:34 → 16:57 = 23 min (dont un retry sur Batch 1)
Batch 8 attempt 1 16:57 → 17:07 = 10 min (timeout, app jamais lancée)
Batch 8 attempt 2 démarre 17:08, coupée par le cap de 60 min du job
Batch 9 jamais atteinte

Donc une suite qui passe ailleurs a été rapportée comme rouge parce qu'on ne l'a jamais laissée finir. Preuve indépendante : ces deux suites sont vertes 9/9 sur main (iOS et Android, backend réel) et 9/9 en local sur émulateur dédié.

Correctif : timeout-minutes 60 → 90 (d3c3064)

Cette branche a consolidé 12 suites iOS en 7 batches précisément pour tenir dans 60 minutes ; #153 en a ajouté deux. Un seul flake de lancement coûte 600 s + un retry, et un tel retry sur un batch précoce suffit à épuiser le budget avant la dernière suite. 90 minutes redonnent au retry la marge pour faire son travail au lieu d'être tronqué par le cap.

Même cap relevé côté Android par symétrie — il fait maintenant tourner 14 suites.

Rien changé aux suites elles-mêmes.

@macminisupport
macminisupport merged commit 6e9cb4c into main Sep 7, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants