From da76d26f2be3b3b09a8fbee0743ede32238796d1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 16:39:29 +0200 Subject: [PATCH 01/12] Add network degradation profiles and enhance bandwidth throttling - Add profiles.yml with predefined DDIL profiles (lte, slow_3g, edge_2g, satellite, flapping) - Enhance bandwidth-throttling action with profile support and packet loss simulation - Add flapping.sh script for intermittent connectivity simulation Made-with: Cursor --- .../actions/bandwidth-throttling/action.yml | 168 +++++++++++++++--- .../actions/bandwidth-throttling/flapping.sh | 77 ++++++++ .../actions/bandwidth-throttling/profiles.yml | 87 +++++++++ 3 files changed, 305 insertions(+), 27 deletions(-) create mode 100755 .github/actions/bandwidth-throttling/flapping.sh create mode 100644 .github/actions/bandwidth-throttling/profiles.yml diff --git a/.github/actions/bandwidth-throttling/action.yml b/.github/actions/bandwidth-throttling/action.yml index 70cb4b8c7e..8f752c80da 100644 --- a/.github/actions/bandwidth-throttling/action.yml +++ b/.github/actions/bandwidth-throttling/action.yml @@ -1,71 +1,185 @@ name: bandwidth-throttling -description: Action to throttle the bandwidth on MacOS runner +description: Action to throttle the bandwidth on MacOS runner with support for network profiles inputs: test_server_host: description: The host of the test server, no protocol required: true + profile: + description: "Network profile to use (lte, slow_3g, edge_2g, satellite, flapping). Overrides individual settings." + required: false + default: "" download_speed: - description: The download speed limit (in Kbit/s) + description: The download speed limit (in Kbit/s) - used if profile is not set required: false default: "3300" upload_speed: - description: The upload speed limit (in Kbit/s) + description: The upload speed limit (in Kbit/s) - used if profile is not set required: false default: "3300" latency: - description: The latency (in ms) each way + description: The latency (in ms) each way - used if profile is not set required: false default: "500" + packet_loss: + description: Packet loss percentage (0-100) - used if profile is not set + required: false + default: "0" disable: description: Disable throttling required: false default: "false" +outputs: + effective_profile: + description: The profile that was applied + value: ${{ steps.resolve-profile.outputs.profile }} + timeout_multiplier: + description: Recommended timeout multiplier for this profile + value: ${{ steps.resolve-profile.outputs.timeout_multiplier }} + runs: using: composite steps: - - name: disable first + - name: Resolve network profile settings + id: resolve-profile + shell: bash + run: | + PROFILE="${{ inputs.profile }}" + + # Profile-based settings + case "$PROFILE" in + lte) + DOWNLOAD=10000 + UPLOAD=5000 + LATENCY=30 + PACKET_LOSS=0 + TIMEOUT_MULT=1 + ;; + slow_3g) + DOWNLOAD=400 + UPLOAD=128 + LATENCY=300 + PACKET_LOSS=2 + TIMEOUT_MULT=3 + ;; + edge_2g) + DOWNLOAD=50 + UPLOAD=25 + LATENCY=500 + PACKET_LOSS=5 + TIMEOUT_MULT=10 + ;; + satellite) + DOWNLOAD=1000 + UPLOAD=256 + LATENCY=700 + PACKET_LOSS=1 + TIMEOUT_MULT=5 + ;; + flapping) + # Base settings for flapping - actual flapping handled separately + DOWNLOAD=1000 + UPLOAD=256 + LATENCY=200 + PACKET_LOSS=0 + TIMEOUT_MULT=5 + ;; + *) + # Use individual inputs if no profile specified + DOWNLOAD=${{ inputs.download_speed }} + UPLOAD=${{ inputs.upload_speed }} + LATENCY=${{ inputs.latency }} + PACKET_LOSS=${{ inputs.packet_loss }} + TIMEOUT_MULT=1 + PROFILE="custom" + ;; + esac + + echo "download=$DOWNLOAD" >> $GITHUB_OUTPUT + echo "upload=$UPLOAD" >> $GITHUB_OUTPUT + echo "latency=$LATENCY" >> $GITHUB_OUTPUT + echo "packet_loss=$PACKET_LOSS" >> $GITHUB_OUTPUT + echo "timeout_multiplier=$TIMEOUT_MULT" >> $GITHUB_OUTPUT + echo "profile=$PROFILE" >> $GITHUB_OUTPUT + + echo "Network profile: $PROFILE" + echo " Download: ${DOWNLOAD} Kbit/s" + echo " Upload: ${UPLOAD} Kbit/s" + echo " Latency: ${LATENCY} ms" + echo " Packet Loss: ${PACKET_LOSS}%" + echo " Timeout Multiplier: ${TIMEOUT_MULT}x" + + - name: Disable existing throttling first if: ${{ inputs.disable == 'true' }} shell: bash continue-on-error: true run: | - sudo pfctl -d - - sleep 2; + sudo pfctl -d 2>/dev/null || true + sudo dnctl -q flush 2>/dev/null || true + sudo dnctl -q pipe flush 2>/dev/null || true + sleep 2 - - name: throttle bandwidth down + - name: Apply network throttling + if: ${{ inputs.disable != 'true' }} shell: bash run: | - # reset pf and dnctl - sudo dnctl -q flush - sudo dnctl -q pipe flush - sudo pfctl -f /etc/pf.conf - sudo pfctl -E - - sleep 2; + # Reset pf and dnctl + sudo dnctl -q flush 2>/dev/null || true + sudo dnctl -q pipe flush 2>/dev/null || true + sudo pfctl -f /etc/pf.conf 2>/dev/null || true + sudo pfctl -E 2>/dev/null || true - sudo pfctl -d - sudo dnctl -q flush - sudo dnctl -q pipe flush + sleep 2 + sudo pfctl -d 2>/dev/null || true + sudo dnctl -q flush 2>/dev/null || true + sudo dnctl -q pipe flush 2>/dev/null || true + # Set up packet filter rules echo "dummynet in from ${{ inputs.test_server_host }} to ! 127.0.0.1 pipe 1 dummynet out from ! 127.0.0.1 to ${{ inputs.test_server_host }} pipe 2" | sudo pfctl -f - - # pipe 1 is download - sudo dnctl pipe 1 config bw ${{ inputs.download_speed }}Kbit/s delay ${{ inputs.latency }}ms + # Configure download pipe (pipe 1) + DOWNLOAD_OPTS="bw ${{ steps.resolve-profile.outputs.download }}Kbit/s delay ${{ steps.resolve-profile.outputs.latency }}ms" + if [ "${{ steps.resolve-profile.outputs.packet_loss }}" != "0" ]; then + # Convert percentage to decimal (e.g., 2% -> 0.02) + PLR=$(echo "scale=4; ${{ steps.resolve-profile.outputs.packet_loss }} / 100" | bc) + DOWNLOAD_OPTS="$DOWNLOAD_OPTS plr $PLR" + fi + sudo dnctl pipe 1 config $DOWNLOAD_OPTS - # pipe 2 is upload - sudo dnctl pipe 2 config bw ${{ inputs.upload_speed }}Kbit/s delay ${{ inputs.latency }}ms + # Configure upload pipe (pipe 2) + UPLOAD_OPTS="bw ${{ steps.resolve-profile.outputs.upload }}Kbit/s delay ${{ steps.resolve-profile.outputs.latency }}ms" + if [ "${{ steps.resolve-profile.outputs.packet_loss }}" != "0" ]; then + PLR=$(echo "scale=4; ${{ steps.resolve-profile.outputs.packet_loss }} / 100" | bc) + UPLOAD_OPTS="$UPLOAD_OPTS plr $PLR" + fi + sudo dnctl pipe 2 config $UPLOAD_OPTS - sleep 5; + sleep 5 sudo pfctl -E - sleep 5; + sleep 5 + + echo "Network throttling applied:" + echo " Profile: ${{ steps.resolve-profile.outputs.profile }}" + sudo dnctl show + + - name: Start flapping simulation + if: ${{ inputs.profile == 'flapping' && inputs.disable != 'true' }} + shell: bash + run: | + # Start the flapping script in background + nohup ${{ github.action_path }}/flapping.sh "${{ inputs.test_server_host }}" & + FLAPPING_PID=$! + echo "FLAPPING_PID=$FLAPPING_PID" >> $GITHUB_ENV + echo "Started flapping simulation with PID: $FLAPPING_PID" - - name: test curl after throttling + - name: Test connection after throttling + if: ${{ inputs.disable != 'true' }} shell: bash run: | - curl -o /dev/null -m 20 --retry 2 -s -w 'Total: %{time_total}s\n' 'https://${{ inputs.test_server_host }}/api/v4/system/ping?get_server_status=true' + echo "Testing connection with throttling applied..." + curl -o /dev/null -m 30 --retry 2 -s -w 'Total: %{time_total}s\n' 'https://${{ inputs.test_server_host }}/api/v4/system/ping?get_server_status=true' diff --git a/.github/actions/bandwidth-throttling/flapping.sh b/.github/actions/bandwidth-throttling/flapping.sh new file mode 100755 index 0000000000..54bf0c5502 --- /dev/null +++ b/.github/actions/bandwidth-throttling/flapping.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Flapping Network Simulation Script +# Simulates intermittent connectivity by cycling through network states +# +# Usage: ./flapping.sh +# Example: ./flapping.sh mobile-e2e-site-1.test.mattermost.cloud + +set -e + +TEST_SERVER_HOST="$1" + +if [ -z "$TEST_SERVER_HOST" ]; then + echo "Error: test_server_host is required" + exit 1 +fi + +echo "Starting flapping network simulation for host: $TEST_SERVER_HOST" +echo "PID: $$" + +# Function to apply network settings +apply_settings() { + local download=$1 + local upload=$2 + local latency=$3 + local packet_loss=$4 + local state_name=$5 + + echo "[$(date '+%H:%M:%S')] Switching to state: $state_name" + + # Reset pipes + sudo dnctl -q pipe flush 2>/dev/null || true + + if [ "$state_name" = "disconnected" ]; then + # 100% packet loss = disconnected + sudo dnctl pipe 1 config bw 1Kbit/s plr 1.0 + sudo dnctl pipe 2 config bw 1Kbit/s plr 1.0 + else + # Configure with specified settings + DOWNLOAD_OPTS="bw ${download}Kbit/s delay ${latency}ms" + UPLOAD_OPTS="bw ${upload}Kbit/s delay ${latency}ms" + + if [ "$packet_loss" != "0" ]; then + PLR=$(echo "scale=4; $packet_loss / 100" | bc) + DOWNLOAD_OPTS="$DOWNLOAD_OPTS plr $PLR" + UPLOAD_OPTS="$UPLOAD_OPTS plr $PLR" + fi + + sudo dnctl pipe 1 config $DOWNLOAD_OPTS + sudo dnctl pipe 2 config $UPLOAD_OPTS + fi +} + +# Flapping pattern loop +# Pattern: connected(30s) -> disconnected(5s) -> slow_3g(30s) -> disconnected(3s) -> repeat +cycle=0 +while true; do + cycle=$((cycle + 1)) + echo "[$(date '+%H:%M:%S')] === Flapping cycle $cycle ===" + + # State 1: Connected (good connection) + apply_settings 1000 256 200 0 "connected" + sleep 30 + + # State 2: Disconnected + apply_settings 0 0 0 100 "disconnected" + sleep 5 + + # State 3: Slow 3G + apply_settings 400 128 300 2 "slow_3g" + sleep 30 + + # State 4: Brief disconnection + apply_settings 0 0 0 100 "disconnected" + sleep 3 + + # Back to connected for next cycle +done diff --git a/.github/actions/bandwidth-throttling/profiles.yml b/.github/actions/bandwidth-throttling/profiles.yml new file mode 100644 index 0000000000..edf6ecff7d --- /dev/null +++ b/.github/actions/bandwidth-throttling/profiles.yml @@ -0,0 +1,87 @@ +# Network Degradation Profiles for DDIL Testing +# These profiles simulate various real-world network conditions +# +# Usage: Pass the profile name to the bandwidth-throttling action +# Example: profile: slow_3g +# +# Reference: +# - ITU-T G.114: 0-150ms one-way delay preferred, 150-400ms acceptable +# - STANAG 5066: HF radio data communications (2.4-9.6 kbps typical) +# - 3GPP specs: Mission-critical communications requirements + +profiles: + # Baseline - good LTE connection (for comparison) + lte: + description: "Good LTE/4G connection - baseline for comparison" + download_kbps: 10000 + upload_kbps: 5000 + latency_ms: 30 + packet_loss_percent: 0 + use_case: "Urban 4G, optimal conditions" + + # Degraded 3G - rural or congested areas + slow_3g: + description: "Slow 3G - rural or congested network" + download_kbps: 400 + upload_kbps: 128 + latency_ms: 300 + packet_loss_percent: 2 + use_case: "Rural coverage, network congestion, indoor" + + # Edge/2G - minimal connectivity + edge_2g: + description: "Edge/2G - minimal cellular connectivity" + download_kbps: 50 + upload_kbps: 25 + latency_ms: 500 + packet_loss_percent: 5 + use_case: "Remote areas, basement, elevator transition" + + # Satellite - high latency + satellite: + description: "GEO Satellite - high latency connection" + download_kbps: 1000 + upload_kbps: 256 + latency_ms: 700 + packet_loss_percent: 1 + use_case: "Maritime, aviation, remote installations" + + # Flapping - intermittent connectivity (special handling required) + flapping: + description: "Intermittent connectivity - connection drops and recovers" + # Base settings when connected + download_kbps: 1000 + upload_kbps: 256 + latency_ms: 200 + packet_loss_percent: 0 + # Flapping pattern (handled by flapping.sh script) + pattern: + - state: "connected" + duration_sec: 30 + - state: "disconnected" + duration_sec: 5 + - state: "slow_3g" + duration_sec: 30 + - state: "disconnected" + duration_sec: 3 + - state: "connected" + duration_sec: 30 + use_case: "Tunnel, elevator, train, moving vehicle" + +# Timeout multipliers for each profile +# Tests should multiply their standard timeouts by this factor +timeout_multipliers: + lte: 1 + slow_3g: 3 + edge_2g: 10 + satellite: 5 + flapping: 5 + +# Recommended test subsets per profile +# Running full test suite under severe degradation is too slow +test_scope: + lte: "full" # Run all tests + slow_3g: "critical" # Run critical path only + edge_2g: "minimal" # Run minimal smoke tests + satellite: "critical" # Run critical path only + flapping: "minimal" # Run minimal smoke tests From 2bb02fe06e5252a85b7ba9acb589318f2e8cfaa7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 16:39:35 +0200 Subject: [PATCH 02/12] Add Linux bandwidth throttling action for Android E2E - Add bandwidth-throttling-linux action using tc/netem - Support same network profiles as macOS version - Add flapping simulation script for Linux Made-with: Cursor --- .../bandwidth-throttling-linux/action.yml | 168 ++++++++++++++++++ .../flapping-linux.sh | 63 +++++++ 2 files changed, 231 insertions(+) create mode 100644 .github/actions/bandwidth-throttling-linux/action.yml create mode 100755 .github/actions/bandwidth-throttling-linux/flapping-linux.sh diff --git a/.github/actions/bandwidth-throttling-linux/action.yml b/.github/actions/bandwidth-throttling-linux/action.yml new file mode 100644 index 0000000000..e633bdb959 --- /dev/null +++ b/.github/actions/bandwidth-throttling-linux/action.yml @@ -0,0 +1,168 @@ +name: bandwidth-throttling-linux +description: Action to throttle the bandwidth on Linux runners (for Android E2E) using tc/netem + +inputs: + test_server_host: + description: The host of the test server, no protocol + required: true + profile: + description: "Network profile to use (lte, slow_3g, edge_2g, satellite, flapping). Overrides individual settings." + required: false + default: "" + download_speed: + description: The download speed limit (in Kbit/s) - used if profile is not set + required: false + default: "3300" + upload_speed: + description: The upload speed limit (in Kbit/s) - used if profile is not set + required: false + default: "3300" + latency: + description: The latency (in ms) each way - used if profile is not set + required: false + default: "500" + packet_loss: + description: Packet loss percentage (0-100) - used if profile is not set + required: false + default: "0" + disable: + description: Disable throttling + required: false + default: "false" + +outputs: + effective_profile: + description: The profile that was applied + value: ${{ steps.resolve-profile.outputs.profile }} + timeout_multiplier: + description: Recommended timeout multiplier for this profile + value: ${{ steps.resolve-profile.outputs.timeout_multiplier }} + +runs: + using: composite + steps: + - name: Resolve network profile settings + id: resolve-profile + shell: bash + run: | + PROFILE="${{ inputs.profile }}" + + # Profile-based settings (same as macOS version) + case "$PROFILE" in + lte) + DOWNLOAD=10000 + UPLOAD=5000 + LATENCY=30 + PACKET_LOSS=0 + TIMEOUT_MULT=1 + ;; + slow_3g) + DOWNLOAD=400 + UPLOAD=128 + LATENCY=300 + PACKET_LOSS=2 + TIMEOUT_MULT=3 + ;; + edge_2g) + DOWNLOAD=50 + UPLOAD=25 + LATENCY=500 + PACKET_LOSS=5 + TIMEOUT_MULT=10 + ;; + satellite) + DOWNLOAD=1000 + UPLOAD=256 + LATENCY=700 + PACKET_LOSS=1 + TIMEOUT_MULT=5 + ;; + flapping) + DOWNLOAD=1000 + UPLOAD=256 + LATENCY=200 + PACKET_LOSS=0 + TIMEOUT_MULT=5 + ;; + *) + DOWNLOAD=${{ inputs.download_speed }} + UPLOAD=${{ inputs.upload_speed }} + LATENCY=${{ inputs.latency }} + PACKET_LOSS=${{ inputs.packet_loss }} + TIMEOUT_MULT=1 + PROFILE="custom" + ;; + esac + + echo "download=$DOWNLOAD" >> $GITHUB_OUTPUT + echo "upload=$UPLOAD" >> $GITHUB_OUTPUT + echo "latency=$LATENCY" >> $GITHUB_OUTPUT + echo "packet_loss=$PACKET_LOSS" >> $GITHUB_OUTPUT + echo "timeout_multiplier=$TIMEOUT_MULT" >> $GITHUB_OUTPUT + echo "profile=$PROFILE" >> $GITHUB_OUTPUT + + echo "Network profile: $PROFILE" + echo " Download: ${DOWNLOAD} Kbit/s" + echo " Upload: ${UPLOAD} Kbit/s" + echo " Latency: ${LATENCY} ms" + echo " Packet Loss: ${PACKET_LOSS}%" + echo " Timeout Multiplier: ${TIMEOUT_MULT}x" + + - name: Disable existing throttling + if: ${{ inputs.disable == 'true' }} + shell: bash + run: | + # Remove any existing tc rules + sudo tc qdisc del dev eth0 root 2>/dev/null || true + echo "Network throttling disabled" + + - name: Apply network throttling with tc/netem + if: ${{ inputs.disable != 'true' }} + shell: bash + run: | + # Remove any existing rules + sudo tc qdisc del dev eth0 root 2>/dev/null || true + + DOWNLOAD="${{ steps.resolve-profile.outputs.download }}" + LATENCY="${{ steps.resolve-profile.outputs.latency }}" + PACKET_LOSS="${{ steps.resolve-profile.outputs.packet_loss }}" + + echo "Applying network throttling..." + + # Build netem options + NETEM_OPTS="delay ${LATENCY}ms" + if [ "$PACKET_LOSS" != "0" ]; then + NETEM_OPTS="$NETEM_OPTS loss ${PACKET_LOSS}%" + fi + + # Add root qdisc with netem for delay and packet loss + sudo tc qdisc add dev eth0 root handle 1: netem $NETEM_OPTS + + # Add tbf (token bucket filter) for rate limiting + # burst = rate / 8 (bytes per second / 8 = 1 second of buffer) + # latency = how long packets can wait in queue + RATE="${DOWNLOAD}kbit" + BURST="$((DOWNLOAD / 8))kb" + + sudo tc qdisc add dev eth0 parent 1: handle 2: tbf rate $RATE burst $BURST latency 50ms + + echo "Network throttling applied:" + echo " Profile: ${{ steps.resolve-profile.outputs.profile }}" + sudo tc qdisc show dev eth0 + + - name: Start flapping simulation + if: ${{ inputs.profile == 'flapping' && inputs.disable != 'true' }} + shell: bash + run: | + # Start the flapping script in background + nohup ${{ github.action_path }}/flapping-linux.sh & + FLAPPING_PID=$! + echo "FLAPPING_PID=$FLAPPING_PID" >> $GITHUB_ENV + echo "Started flapping simulation with PID: $FLAPPING_PID" + + - name: Test connection after throttling + if: ${{ inputs.disable != 'true' }} + shell: bash + run: | + echo "Testing connection with throttling applied..." + curl -o /dev/null -m 30 --retry 2 -s -w 'Total: %{time_total}s\n' 'https://${{ inputs.test_server_host }}/api/v4/system/ping?get_server_status=true' || echo "Connection test completed (may have timed out under heavy throttling)" diff --git a/.github/actions/bandwidth-throttling-linux/flapping-linux.sh b/.github/actions/bandwidth-throttling-linux/flapping-linux.sh new file mode 100755 index 0000000000..7a06bd227a --- /dev/null +++ b/.github/actions/bandwidth-throttling-linux/flapping-linux.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Flapping Network Simulation Script for Linux (tc/netem) +# Simulates intermittent connectivity by cycling through network states + +set -e + +echo "Starting flapping network simulation (Linux)" +echo "PID: $$" + +# Function to apply network settings using tc/netem +apply_settings() { + local download=$1 + local latency=$2 + local packet_loss=$3 + local state_name=$4 + + echo "[$(date '+%H:%M:%S')] Switching to state: $state_name" + + # Remove existing rules + sudo tc qdisc del dev eth0 root 2>/dev/null || true + + if [ "$state_name" = "disconnected" ]; then + # 100% packet loss = disconnected + sudo tc qdisc add dev eth0 root netem loss 100% + else + # Build netem options + NETEM_OPTS="delay ${latency}ms" + if [ "$packet_loss" != "0" ]; then + NETEM_OPTS="$NETEM_OPTS loss ${packet_loss}%" + fi + + # Add netem for delay/loss + sudo tc qdisc add dev eth0 root handle 1: netem $NETEM_OPTS + + # Add rate limiting + RATE="${download}kbit" + BURST="$((download / 8))kb" + sudo tc qdisc add dev eth0 parent 1: handle 2: tbf rate $RATE burst $BURST latency 50ms + fi +} + +# Flapping pattern loop +cycle=0 +while true; do + cycle=$((cycle + 1)) + echo "[$(date '+%H:%M:%S')] === Flapping cycle $cycle ===" + + # State 1: Connected (good connection) + apply_settings 1000 200 0 "connected" + sleep 30 + + # State 2: Disconnected + apply_settings 0 0 100 "disconnected" + sleep 5 + + # State 3: Slow 3G + apply_settings 400 300 2 "slow_3g" + sleep 30 + + # State 4: Brief disconnection + apply_settings 0 0 100 "disconnected" + sleep 3 +done From 6b1d25eb180ef869d9e45cd884668f1d59d01779 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 16:40:45 +0200 Subject: [PATCH 03/12] Add E2E network profile utilities - Add network_profiles.ts with profile constants and helpers - Add getTimeoutMultiplier() for profile-aware test timeouts - Update utils/index.ts to use profile-based timeout scaling Made-with: Cursor --- detox/e2e/support/utils/index.ts | 9 +- detox/e2e/support/utils/network_profiles.ts | 147 ++++++++++++++++++++ 2 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 detox/e2e/support/utils/network_profiles.ts diff --git a/detox/e2e/support/utils/index.ts b/detox/e2e/support/utils/index.ts index 892da83782..8f1db4f5f3 100644 --- a/detox/e2e/support/utils/index.ts +++ b/detox/e2e/support/utils/index.ts @@ -6,6 +6,7 @@ import {v4 as uuidv4} from 'uuid'; export * from './email'; export * from './detoxhelpers'; +export * from './network_profiles'; /** * Explicit `wait` should not normally used but made available for special cases. @@ -63,8 +64,12 @@ export const getAdminAccount = () => { }; }; -const SECOND = 1000 * (process.env.LOW_BANDWIDTH_MODE === 'true' ? 5 : 1); -const MINUTE = 60 * 1000; +import {getTimeoutMultiplier} from './network_profiles'; + +// Get timeout multiplier from network profile (supports both new NETWORK_PROFILE and legacy LOW_BANDWIDTH_MODE) +const TIMEOUT_MULTIPLIER = getTimeoutMultiplier(); +const SECOND = 1000 * TIMEOUT_MULTIPLIER; +const MINUTE = 60 * 1000 * TIMEOUT_MULTIPLIER; export const timeouts = { HALF_SEC: SECOND / 2, diff --git a/detox/e2e/support/utils/network_profiles.ts b/detox/e2e/support/utils/network_profiles.ts new file mode 100644 index 0000000000..4921958b55 --- /dev/null +++ b/detox/e2e/support/utils/network_profiles.ts @@ -0,0 +1,147 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/** + * Network degradation profiles for DDIL testing. + * These match the profiles defined in .github/actions/bandwidth-throttling/profiles.yml + */ + +export type NetworkProfile = 'lte' | 'slow_3g' | 'edge_2g' | 'satellite' | 'flapping' | 'custom'; + +export interface NetworkProfileConfig { + name: NetworkProfile; + description: string; + downloadKbps: number; + uploadKbps: number; + latencyMs: number; + packetLossPercent: number; + timeoutMultiplier: number; +} + +export const NETWORK_PROFILES: Record = { + lte: { + name: 'lte', + description: 'Good LTE/4G connection - baseline for comparison', + downloadKbps: 10000, + uploadKbps: 5000, + latencyMs: 30, + packetLossPercent: 0, + timeoutMultiplier: 1, + }, + slow_3g: { + name: 'slow_3g', + description: 'Slow 3G - rural or congested network', + downloadKbps: 400, + uploadKbps: 128, + latencyMs: 300, + packetLossPercent: 2, + timeoutMultiplier: 3, + }, + edge_2g: { + name: 'edge_2g', + description: 'Edge/2G - minimal cellular connectivity', + downloadKbps: 50, + uploadKbps: 25, + latencyMs: 500, + packetLossPercent: 5, + timeoutMultiplier: 10, + }, + satellite: { + name: 'satellite', + description: 'GEO Satellite - high latency connection', + downloadKbps: 1000, + uploadKbps: 256, + latencyMs: 700, + packetLossPercent: 1, + timeoutMultiplier: 5, + }, + flapping: { + name: 'flapping', + description: 'Intermittent connectivity - connection drops and recovers', + downloadKbps: 1000, + uploadKbps: 256, + latencyMs: 200, + packetLossPercent: 0, + timeoutMultiplier: 5, + }, + custom: { + name: 'custom', + description: 'Custom profile with manual settings', + downloadKbps: 3300, + uploadKbps: 3300, + latencyMs: 500, + packetLossPercent: 0, + timeoutMultiplier: 1, + }, +}; + +/** + * Get the current network profile from environment variables. + * Falls back to 'lte' (baseline) if not set. + */ +export function getCurrentNetworkProfile(): NetworkProfile { + const envProfile = process.env.NETWORK_PROFILE as NetworkProfile; + if (envProfile && NETWORK_PROFILES[envProfile]) { + return envProfile; + } + + // Backward compatibility with LOW_BANDWIDTH_MODE + if (process.env.LOW_BANDWIDTH_MODE === 'true') { + return 'slow_3g'; + } + + return 'lte'; +} + +/** + * Get the timeout multiplier for the current network profile. + * Use this to scale test timeouts based on network conditions. + */ +export function getTimeoutMultiplier(): number { + const profile = getCurrentNetworkProfile(); + return NETWORK_PROFILES[profile].timeoutMultiplier; +} + +/** + * Get the full configuration for the current network profile. + */ +export function getCurrentNetworkConfig(): NetworkProfileConfig { + const profile = getCurrentNetworkProfile(); + return NETWORK_PROFILES[profile]; +} + +/** + * Check if we're running under degraded network conditions. + */ +export function isDegradedNetwork(): boolean { + const profile = getCurrentNetworkProfile(); + return profile !== 'lte'; +} + +/** + * Check if we're running under severely degraded network conditions. + * This is useful for skipping tests that are not meaningful under extreme degradation. + */ +export function isSeverelyDegradedNetwork(): boolean { + const profile = getCurrentNetworkProfile(); + return profile === 'edge_2g' || profile === 'flapping'; +} + +/** + * Get a formatted string with current network profile information. + * Useful at the start of test runs for debugging. + */ +export function getNetworkProfileInfo(): string { + const config = getCurrentNetworkConfig(); + return [ + '=== Network Profile ===', + `Profile: ${config.name}`, + `Description: ${config.description}`, + `Download: ${config.downloadKbps} Kbps`, + `Upload: ${config.uploadKbps} Kbps`, + `Latency: ${config.latencyMs} ms`, + `Packet Loss: ${config.packetLossPercent}%`, + `Timeout Multiplier: ${config.timeoutMultiplier}x`, + '=======================', + ].join('\n'); +} From b3b1f1fa3656dd80fefbf1e5df0f679780c59691 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 16:40:51 +0200 Subject: [PATCH 04/12] Add network profile support to E2E workflow templates - Update prepare-low-bandwidth action with profile input - Add network_profile input to iOS and Android templates - Pass NETWORK_PROFILE env var to test runs - Add Linux bandwidth throttling integration for Android Made-with: Cursor --- .../actions/prepare-low-bandwidth/action.yml | 49 ++++++++++++++----- .github/workflows/e2e-android-template.yml | 37 +++++++++++++- .github/workflows/e2e-ios-template.yml | 30 ++++++++---- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/.github/actions/prepare-low-bandwidth/action.yml b/.github/actions/prepare-low-bandwidth/action.yml index b39b77c27a..a2b9a0041e 100644 --- a/.github/actions/prepare-low-bandwidth/action.yml +++ b/.github/actions/prepare-low-bandwidth/action.yml @@ -1,5 +1,5 @@ name: Prepare Low Bandwidth Environment (MacOS & iOS Simulators only) -description: prepare any workflow for low bandwidth testing +description: Prepare any workflow for low bandwidth or degraded network testing inputs: test_server_url: @@ -8,60 +8,85 @@ inputs: device_name: description: The iOS simulator name required: true + network_profile: + description: "Network profile to use (lte, slow_3g, edge_2g, satellite, flapping)" + required: false + default: "" download_speed: - description: The download speed limit (in Kbit/s) + description: The download speed limit (in Kbit/s) - used if profile is not set required: false default: "3300" upload_speed: - description: The upload speed limit (in Kbit/s) + description: The upload speed limit (in Kbit/s) - used if profile is not set required: false default: "3300" latency: - description: The latency (in ms) each way + description: The latency (in ms) each way - used if profile is not set required: false default: "500" +outputs: + effective_profile: + description: The network profile that was applied + value: ${{ steps.throttle-bandwidth.outputs.effective_profile }} + timeout_multiplier: + description: Recommended timeout multiplier for this profile + value: ${{ steps.throttle-bandwidth.outputs.timeout_multiplier }} runs: using: composite steps: - - name: delete the zip file and trash (to free up space) + - name: Delete the zip file and trash (to free up space) shell: bash run: | rm -rf mobile-artifacts/*.zip sudo rm -rf ~/.Trash/* - - name: check disk space + - name: Check disk space shell: bash run: df -h - - name: remove protocol from SITE_1_URL + - name: Remove protocol from test server URL id: remove-protocol shell: bash run: | echo "SITE_1_HOST=${{ inputs.test_server_url }}" | sed -e 's/http:\/\///g' -e 's/https:\/\///g' >> ${GITHUB_OUTPUT} - - name: Throttle Bandwidth 1 + - name: Throttle Bandwidth (attempt 1) id: throttle-bandwidth-1 continue-on-error: true uses: ./.github/actions/bandwidth-throttling with: test_server_host: ${{ steps.remove-protocol.outputs.SITE_1_HOST }} + profile: ${{ inputs.network_profile }} download_speed: ${{ inputs.download_speed }} upload_speed: ${{ inputs.upload_speed }} latency: ${{ inputs.latency }} - - name: Throttle Bandwidth 2 + - name: Throttle Bandwidth (attempt 2 with reset) if: steps.throttle-bandwidth-1.outcome != 'success' id: throttle-bandwidth-2 uses: ./.github/actions/bandwidth-throttling with: test_server_host: ${{ steps.remove-protocol.outputs.SITE_1_HOST }} - download_speed: ${{ inputs.download_speed}} + profile: ${{ inputs.network_profile }} + download_speed: ${{ inputs.download_speed }} upload_speed: ${{ inputs.upload_speed }} latency: ${{ inputs.latency }} disable: "true" + - name: Set throttle outputs + id: throttle-bandwidth + shell: bash + run: | + if [ "${{ steps.throttle-bandwidth-1.outcome }}" == "success" ]; then + echo "effective_profile=${{ steps.throttle-bandwidth-1.outputs.effective_profile }}" >> $GITHUB_OUTPUT + echo "timeout_multiplier=${{ steps.throttle-bandwidth-1.outputs.timeout_multiplier }}" >> $GITHUB_OUTPUT + else + echo "effective_profile=${{ steps.throttle-bandwidth-2.outputs.effective_profile }}" >> $GITHUB_OUTPUT + echo "timeout_multiplier=${{ steps.throttle-bandwidth-2.outputs.timeout_multiplier }}" >> $GITHUB_OUTPUT + fi + - name: Install mitmproxy & pm2 (process manager) id: install-mitmproxy-pm2 shell: bash @@ -89,7 +114,7 @@ runs: simulator_udid=$(xcrun simctl list devices "${{ inputs.device_name }}" -j | jq '.devices' | jq '."com.apple.CoreSimulator.SimRuntime.iOS-17-4"[0]["udid"]') echo "simulator_udid="$(echo $simulator_udid) >> ${GITHUB_OUTPUT} - - name: install certificate + - name: Install certificate shell: bash run: | sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem @@ -101,6 +126,6 @@ runs: sleep 5; - - name: show me booted simulators + - name: Show booted simulators shell: bash run: xcrun simctl list devices booted | grep Booted diff --git a/.github/workflows/e2e-android-template.yml b/.github/workflows/e2e-android-template.yml index f1ac8ff2ce..81371ad273 100644 --- a/.github/workflows/e2e-android-template.yml +++ b/.github/workflows/e2e-android-template.yml @@ -39,10 +39,20 @@ on: type: string default: 'false' low_bandwidth_mode: - description: "Enable low bandwidth mode" + description: "Enable low bandwidth mode (legacy - prefer network_profile)" required: false type: boolean default: false + network_profile: + description: "Network degradation profile (lte, slow_3g, edge_2g, satellite, flapping)" + required: false + type: string + default: "" + test_filter: + description: "Test filter pattern (e.g., 'degradation' to run only degradation tests)" + required: false + type: string + default: "" android_avd_name: description: "Android Emulator name" required: false @@ -218,12 +228,37 @@ jobs: env: JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} + - name: Remove protocol from test server URL + id: remove-protocol + if: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' }} + run: | + echo "SITE_1_HOST=${{ env.SITE_1_URL }}" | sed -e 's/http:\/\///g' -e 's/https:\/\///g' >> ${GITHUB_OUTPUT} + + - name: Apply Network Throttling + if: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' }} + uses: ./.github/actions/bandwidth-throttling-linux + with: + test_server_host: ${{ steps.remove-protocol.outputs.SITE_1_HOST }} + profile: ${{ inputs.network_profile }} + download_speed: "3300" + upload_speed: "3300" + latency: "500" + - name: Create and run Android Emulator run: | cd detox chmod +x ./create_android_emulator.sh CI=true ./create_android_emulator.sh ${{ env.SDK_VERSION }} ${{ env.AVD_NAME }} ${{ matrix.specs }} continue-on-error: true # We want to run all the tests + env: + LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' }} + NETWORK_PROFILE: ${{ inputs.network_profile }} + + - name: Reset Network Settings + if: ${{ (inputs.low_bandwidth_mode || inputs.network_profile != '') && always() }} + run: | + sudo tc qdisc del dev eth0 root 2>/dev/null || true + echo "Network throttling disabled" - name: Upload Android Test Report if: always() diff --git a/.github/workflows/e2e-ios-template.yml b/.github/workflows/e2e-ios-template.yml index 63346be8be..c0470ceb07 100644 --- a/.github/workflows/e2e-ios-template.yml +++ b/.github/workflows/e2e-ios-template.yml @@ -45,10 +45,20 @@ on: type: string default: "iOS 26.2" low_bandwidth_mode: - description: "Enable low bandwidth mode" + description: "Enable low bandwidth mode (legacy - prefer network_profile)" required: false type: boolean default: false + network_profile: + description: "Network degradation profile (lte, slow_3g, edge_2g, satellite, flapping)" + required: false + type: string + default: "" + test_filter: + description: "Test filter pattern (e.g., 'degradation' to run only degradation tests)" + required: false + type: string + default: "" outputs: STATUS: value: ${{ jobs.generate-report.outputs.STATUS }} @@ -124,7 +134,7 @@ jobs: name: machine-${{ matrix.runId }}-os-${{ matrix.deviceOsVersion }} runs-on: macos-15 continue-on-error: true - timeout-minutes: ${{ inputs.low_bandwidth_mode && 240 || 180 }} + timeout-minutes: ${{ (inputs.low_bandwidth_mode || inputs.network_profile != '') && 240 || 180 }} env: IOS: true needs: @@ -158,14 +168,15 @@ jobs: # delete zip file rm -f mobile-artifacts/*.zip - - name: Prepare Low Bandwidth Environment + - name: Prepare Degraded Network Environment id: prepare-low-bandwidth uses: ./.github/actions/prepare-low-bandwidth - if: ${{ inputs.low_bandwidth_mode }} + if: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' }} with: test_server_url: ${{ env.SITE_1_URL }} device_name: ${{ env.DEVICE_NAME }} - # all these value should be configurable + network_profile: ${{ inputs.network_profile }} + # Fallback values for legacy low_bandwidth_mode (when no profile specified) download_speed: "3300" upload_speed: "3300" latency: "500" @@ -250,7 +261,7 @@ jobs: echo "SIMULATOR_ID=$SIMULATOR_ID" >> $GITHUB_ENV - name: Start Proxy - if: ${{ inputs.low_bandwidth_mode }} + if: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' }} id: start-proxy uses: ./.github/actions/start-proxy with: @@ -333,7 +344,8 @@ jobs: DETOX_LOGLEVEL: "debug" DETOX_DEVICE_TYPE: ${{ env.DEVICE_NAME }} DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }} - LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode }} + LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' }} + NETWORK_PROFILE: ${{ inputs.network_profile }} - name: Cleanup Simulator State if: always() @@ -356,7 +368,7 @@ jobs: echo "✅ Simulator cleanup complete" - name: reset network settings - if: ${{ inputs.low_bandwidth_mode || failure() }} + if: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' || failure() }} run: | networksetup -setwebproxystate Ethernet "off" networksetup -setsecurewebproxystate Ethernet "off" @@ -367,7 +379,7 @@ jobs: sleep 5; - name: Upload mitmdump Flow Output - if: ${{ inputs.low_bandwidth_mode }} + if: ${{ inputs.low_bandwidth_mode || inputs.network_profile != '' }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ios-mitmdump-flow-output-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }} From e02f7a720028084be86dc55dad12f6d29a6e19fa Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 16:40:56 +0200 Subject: [PATCH 05/12] Add nightly and weekly degradation test workflows - Add e2e-degradation-nightly.yml running slow_3g and edge_2g profiles daily - Add e2e-degradation-weekly.yml running flapping profile on Sundays - Support both iOS and Android platforms - Include workflow_dispatch for manual triggering Made-with: Cursor --- .github/workflows/e2e-degradation-nightly.yml | 214 ++++++++++++++++++ .github/workflows/e2e-degradation-weekly.yml | 167 ++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 .github/workflows/e2e-degradation-nightly.yml create mode 100644 .github/workflows/e2e-degradation-weekly.yml diff --git a/.github/workflows/e2e-degradation-nightly.yml b/.github/workflows/e2e-degradation-nightly.yml new file mode 100644 index 0000000000..be42381773 --- /dev/null +++ b/.github/workflows/e2e-degradation-nightly.yml @@ -0,0 +1,214 @@ +# Nightly E2E tests under degraded network conditions (DDIL testing) +# Runs a matrix of network profiles to validate app behavior under various conditions + +name: E2E Degradation Tests (Nightly) + +on: + schedule: + # Run at 2 AM UTC every day + - cron: '0 2 * * *' + workflow_dispatch: + inputs: + profiles: + description: 'Network profiles to test (comma-separated: slow_3g,edge_2g,satellite)' + required: false + default: 'slow_3g,edge_2g' + type: string + platforms: + description: 'Platforms to test (ios,android,both)' + required: false + default: 'ios' + type: choice + options: + - ios + - android + - both + +env: + MOBILE_VERSION: ${{ github.ref }} + +jobs: + # Parse input profiles into matrix + setup-matrix: + runs-on: ubuntu-22.04 + outputs: + profiles: ${{ steps.parse.outputs.profiles }} + run_ios: ${{ steps.parse.outputs.run_ios }} + run_android: ${{ steps.parse.outputs.run_android }} + steps: + - name: Parse profiles and platforms + id: parse + run: | + # Default profiles for scheduled runs + PROFILES="${{ github.event.inputs.profiles || 'slow_3g,edge_2g' }}" + + # Convert comma-separated to JSON array + PROFILES_JSON=$(echo "$PROFILES" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$";""))' -c) + echo "profiles=$PROFILES_JSON" >> $GITHUB_OUTPUT + + # Determine platforms + PLATFORMS="${{ github.event.inputs.platforms || 'ios' }}" + if [ "$PLATFORMS" = "ios" ] || [ "$PLATFORMS" = "both" ]; then + echo "run_ios=true" >> $GITHUB_OUTPUT + else + echo "run_ios=false" >> $GITHUB_OUTPUT + fi + + if [ "$PLATFORMS" = "android" ] || [ "$PLATFORMS" = "both" ]; then + echo "run_android=true" >> $GITHUB_OUTPUT + else + echo "run_android=false" >> $GITHUB_OUTPUT + fi + + echo "Network profiles to test: $PROFILES_JSON" + echo "Platforms: $PLATFORMS" + + # Build iOS simulator app once for all profiles + build-ios-simulator: + if: ${{ needs.setup-matrix.outputs.run_ios == 'true' }} + runs-on: macos-26 + needs: setup-matrix + steps: + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Prepare iOS Build + uses: ./.github/actions/prepare-ios-build + with: + intune-enabled: 'false' + + - name: Set .env with RUNNING_E2E=true + run: echo "RUNNING_E2E=true" > .env + + - name: Build iOS Simulator + env: + TAG: "${{ github.sha }}" + GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}" + run: bundle exec fastlane ios simulator --env ios.simulator skip_upload_to_s3_bucket:true + working-directory: ./fastlane + + - name: Upload iOS Simulator Build + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ios-build-simulator-${{ github.run_id }} + path: Mattermost-simulator-*.app.zip + + # Build Android APK once for all profiles + build-android-apk: + if: ${{ needs.setup-matrix.outputs.run_android == 'true' }} + runs-on: ubuntu-latest-8-cores + needs: setup-matrix + env: + ORG_GRADLE_PROJECT_jvmargs: -Xmx8g + steps: + - name: Prune Docker to free up space + run: docker system prune -af + + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Prepare Android Build + uses: ./.github/actions/prepare-android-build + env: + STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}" + STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}" + STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}" + MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}" + + - name: Install Dependencies + run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk + + - name: Detox build + run: | + cd detox + npm install + npm install -g detox-cli + npm run e2e:android-inject-settings + npm run e2e:android-build + + - name: Upload Android Build + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: android-build-files-${{ github.run_id }} + path: "android/app/build/**/*" + + # Run iOS tests for each network profile + ios-degradation-tests: + if: ${{ needs.setup-matrix.outputs.run_ios == 'true' }} + needs: + - setup-matrix + - build-ios-simulator + strategy: + fail-fast: false + matrix: + profile: ${{ fromJSON(needs.setup-matrix.outputs.profiles) }} + name: iOS - ${{ matrix.profile }} + uses: ./.github/workflows/e2e-ios-template.yml + with: + run-type: "Nightly-Degradation" + MOBILE_VERSION: ${{ github.sha }} + network_profile: ${{ matrix.profile }} + record_tests_in_zephyr: 'false' + testcase_failure_fatal: false + secrets: inherit + + # Run Android tests for each network profile + android-degradation-tests: + if: ${{ needs.setup-matrix.outputs.run_android == 'true' }} + needs: + - setup-matrix + - build-android-apk + strategy: + fail-fast: false + matrix: + profile: ${{ fromJSON(needs.setup-matrix.outputs.profiles) }} + name: Android - ${{ matrix.profile }} + uses: ./.github/workflows/e2e-android-template.yml + with: + run-android-tests: true + run-type: "Nightly-Degradation" + MOBILE_VERSION: ${{ github.sha }} + network_profile: ${{ matrix.profile }} + record_tests_in_zephyr: 'false' + testcase_failure_fatal: false + secrets: inherit + + # Aggregate results and send notification + summary: + runs-on: ubuntu-22.04 + needs: + - setup-matrix + - ios-degradation-tests + - android-degradation-tests + if: always() + steps: + - name: Generate Summary + run: | + echo "# DDIL/Degradation Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Network Profiles Tested" >> $GITHUB_STEP_SUMMARY + echo '${{ needs.setup-matrix.outputs.profiles }}' | jq -r '.[]' | while read profile; do + echo "- $profile" >> $GITHUB_STEP_SUMMARY + done + echo "" >> $GITHUB_STEP_SUMMARY + + echo "## iOS Results" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.setup-matrix.outputs.run_ios }}" == "true" ]; then + echo "Status: ${{ needs.ios-degradation-tests.result }}" >> $GITHUB_STEP_SUMMARY + else + echo "Skipped" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + echo "## Android Results" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.setup-matrix.outputs.run_android }}" == "true" ]; then + echo "Status: ${{ needs.android-degradation-tests.result }}" >> $GITHUB_STEP_SUMMARY + else + echo "Skipped" >> $GITHUB_STEP_SUMMARY + fi + + - name: Send Notification on Failure + if: ${{ failure() }} + run: | + echo "Degradation tests had failures - notification would be sent here" + # Add webhook notification if needed diff --git a/.github/workflows/e2e-degradation-weekly.yml b/.github/workflows/e2e-degradation-weekly.yml new file mode 100644 index 0000000000..5a10ea9c59 --- /dev/null +++ b/.github/workflows/e2e-degradation-weekly.yml @@ -0,0 +1,167 @@ +# Weekly E2E tests with flapping/intermittent network simulation +# Tests reconnection handling, message queue recovery, and race conditions +# during network state transitions + +name: E2E Degradation Tests (Weekly - Flapping) + +on: + schedule: + # Run at 3 AM UTC every Sunday + - cron: '0 3 * * 0' + workflow_dispatch: + inputs: + platforms: + description: 'Platforms to test' + required: false + default: 'ios' + type: choice + options: + - ios + - android + - both + +env: + MOBILE_VERSION: ${{ github.ref }} + +jobs: + setup: + runs-on: ubuntu-22.04 + outputs: + run_ios: ${{ steps.parse.outputs.run_ios }} + run_android: ${{ steps.parse.outputs.run_android }} + steps: + - name: Parse platforms + id: parse + run: | + PLATFORMS="${{ github.event.inputs.platforms || 'ios' }}" + if [ "$PLATFORMS" = "ios" ] || [ "$PLATFORMS" = "both" ]; then + echo "run_ios=true" >> $GITHUB_OUTPUT + else + echo "run_ios=false" >> $GITHUB_OUTPUT + fi + if [ "$PLATFORMS" = "android" ] || [ "$PLATFORMS" = "both" ]; then + echo "run_android=true" >> $GITHUB_OUTPUT + else + echo "run_android=false" >> $GITHUB_OUTPUT + fi + + build-ios-simulator: + if: ${{ needs.setup.outputs.run_ios == 'true' }} + runs-on: macos-26 + needs: setup + steps: + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Prepare iOS Build + uses: ./.github/actions/prepare-ios-build + with: + intune-enabled: 'false' + + - name: Set .env with RUNNING_E2E=true + run: echo "RUNNING_E2E=true" > .env + + - name: Build iOS Simulator + env: + TAG: "${{ github.sha }}" + GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}" + run: bundle exec fastlane ios simulator --env ios.simulator skip_upload_to_s3_bucket:true + working-directory: ./fastlane + + - name: Upload iOS Simulator Build + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ios-build-simulator-${{ github.run_id }} + path: Mattermost-simulator-*.app.zip + + build-android-apk: + if: ${{ needs.setup.outputs.run_android == 'true' }} + runs-on: ubuntu-latest-8-cores + needs: setup + env: + ORG_GRADLE_PROJECT_jvmargs: -Xmx8g + steps: + - name: Prune Docker to free up space + run: docker system prune -af + + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Prepare Android Build + uses: ./.github/actions/prepare-android-build + env: + STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}" + STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}" + STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}" + MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}" + + - name: Install Dependencies + run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk + + - name: Detox build + run: | + cd detox + npm install + npm install -g detox-cli + npm run e2e:android-inject-settings + npm run e2e:android-build + + - name: Upload Android Build + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: android-build-files-${{ github.run_id }} + path: "android/app/build/**/*" + + # Flapping tests - intermittent connectivity + ios-flapping-tests: + if: ${{ needs.setup.outputs.run_ios == 'true' }} + needs: + - setup + - build-ios-simulator + name: iOS - flapping (intermittent) + uses: ./.github/workflows/e2e-ios-template.yml + with: + run-type: "Weekly-Flapping" + MOBILE_VERSION: ${{ github.sha }} + network_profile: flapping + record_tests_in_zephyr: 'false' + testcase_failure_fatal: false + secrets: inherit + + android-flapping-tests: + if: ${{ needs.setup.outputs.run_android == 'true' }} + needs: + - setup + - build-android-apk + name: Android - flapping (intermittent) + uses: ./.github/workflows/e2e-android-template.yml + with: + run-android-tests: true + run-type: "Weekly-Flapping" + MOBILE_VERSION: ${{ github.sha }} + network_profile: flapping + record_tests_in_zephyr: 'false' + testcase_failure_fatal: false + secrets: inherit + + summary: + runs-on: ubuntu-22.04 + needs: + - setup + - ios-flapping-tests + - android-flapping-tests + if: always() + steps: + - name: Generate Summary + run: | + echo "# Weekly Flapping Network Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tests run with **flapping** profile (intermittent connectivity simulation)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Pattern: connected(30s) → disconnected(5s) → slow_3g(30s) → disconnected(3s) → repeat" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Results" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Status |" >> $GITHUB_STEP_SUMMARY + echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| iOS | ${{ needs.ios-flapping-tests.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Android | ${{ needs.android-flapping-tests.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY From 712623c7b59ce207d74d97bb0c1ff5f32b418e77 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 16:41:02 +0200 Subject: [PATCH 06/12] Add degradation E2E test suite - Add message_delivery.e2e.ts for testing message send under degraded conditions - Add connection_recovery.e2e.ts for testing reconnection and backgrounding - Add README.md documenting the degradation testing approach Made-with: Cursor --- detox/e2e/test/degradation/README.md | 55 +++++ .../degradation/connection_recovery.e2e.ts | 191 ++++++++++++++++++ .../test/degradation/message_delivery.e2e.ts | 189 +++++++++++++++++ 3 files changed, 435 insertions(+) create mode 100644 detox/e2e/test/degradation/README.md create mode 100644 detox/e2e/test/degradation/connection_recovery.e2e.ts create mode 100644 detox/e2e/test/degradation/message_delivery.e2e.ts diff --git a/detox/e2e/test/degradation/README.md b/detox/e2e/test/degradation/README.md new file mode 100644 index 0000000000..baebfae254 --- /dev/null +++ b/detox/e2e/test/degradation/README.md @@ -0,0 +1,55 @@ +# Degradation Tests (DDIL Testing) + +This directory contains E2E tests specifically designed to validate app behavior under degraded network conditions (Disconnected, Degraded, Intermittent, and Low-bandwidth environments). + +## Purpose + +These tests focus on behaviors that are critical under poor network conditions: +- Message delivery and eventual consistency +- Reconnection handling +- Offline queue behavior +- UI feedback during slow operations +- Error handling and recovery + +## Network Profiles + +Tests are designed to run under various network profiles defined in `.github/actions/bandwidth-throttling/profiles.yml`: + +| Profile | Download | Latency | Packet Loss | Use Case | +|---------|----------|---------|-------------|----------| +| `slow_3g` | 400 Kbps | 300ms | 2% | Rural/congested areas | +| `edge_2g` | 50 Kbps | 500ms | 5% | Edge coverage | +| `satellite` | 1 Mbps | 700ms | 1% | High-latency satellite | +| `flapping` | varies | varies | varies | Intermittent connectivity | + +## Running Degradation Tests + +### Locally (with network simulation) +```bash +# Use Charles Proxy or similar to throttle network +cd detox +NETWORK_PROFILE=slow_3g npm run e2e:ios-test -- --testPathPattern=degradation +``` + +### In CI +The nightly workflow runs these tests automatically under multiple profiles: +```bash +# Trigger manually via GitHub Actions workflow_dispatch +gh workflow run e2e-degradation-nightly.yml -f profiles=slow_3g,edge_2g -f platforms=ios +``` + +## Test Guidelines + +1. **Use longer timeouts**: Import `getTimeoutMultiplier()` from `@support/utils` and multiply standard timeouts +2. **Expect retries**: Operations may need multiple attempts +3. **Check eventual consistency**: Verify state after allowing time for sync +4. **Validate UI feedback**: Ensure loading states and connection banners appear appropriately +5. **Don't assert timing**: Under degradation, exact timing is unpredictable + +## Expanding Test Coverage + +To add a test to degradation testing: +1. Place it in this `degradation/` directory, or +2. Tag existing tests with `@degradation` in the test name (future feature) + +Start with critical user journeys and expand based on field reports of issues under poor network conditions. diff --git a/detox/e2e/test/degradation/connection_recovery.e2e.ts b/detox/e2e/test/degradation/connection_recovery.e2e.ts new file mode 100644 index 0000000000..6314c1a16e --- /dev/null +++ b/detox/e2e/test/degradation/connection_recovery.e2e.ts @@ -0,0 +1,191 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// ******************************************************************* +// - [#] indicates a test step (e.g. # Go to a screen) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element testID when selecting an element. Create one if none. +// ******************************************************************* + +import { + Post, + Setup, +} from '@support/server_api'; +import { + serverOneUrl, + siteOneUrl, +} from '@support/test_config'; +import { + ChannelScreen, + ChannelListScreen, + HomeScreen, + LoginScreen, + ServerScreen, +} from '@support/ui/screen'; +import { + getRandomId, + timeouts, + wait, + getTimeoutMultiplier, + getNetworkProfileInfo, + getCurrentNetworkProfile, +} from '@support/utils'; +import {expect} from 'detox'; + +describe('Degradation - Connection Recovery', () => { + const serverOneDisplayName = 'Server 1'; + const channelsCategory = 'channels'; + let testChannel: any; + let testUser: any; + const timeoutMultiplier = getTimeoutMultiplier(); + + beforeAll(async () => { + // Log network profile for debugging - visible in test output + // eslint-disable-next-line no-console + console.log(getNetworkProfileInfo()); + + const {channel, user} = await Setup.apiInit(siteOneUrl); + testChannel = channel; + testUser = user; + + // # Log in to server + await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); + await LoginScreen.login(user); + }); + + beforeEach(async () => { + // * Verify on channel list screen + await ChannelListScreen.toBeVisible(); + }); + + afterAll(async () => { + // # Log out + await HomeScreen.logout(); + }); + + it('DDIL-T010 - should recover after app backgrounding', async () => { + // # Open a channel screen + await ChannelScreen.open(channelsCategory, testChannel.name); + + // # Send a message before backgrounding + const beforeMessage = `Before background ${getRandomId()}`; + await ChannelScreen.postMessage(beforeMessage); + + // * Verify message was sent + const {post: beforePost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); + expect(beforePost.message).toBe(beforeMessage); + + // # Background the app + await device.sendToBackground(); + await wait(timeouts.TEN_SEC); + + // # Bring app back to foreground + await device.launchApp({newInstance: false}); + + // * Verify channel is still visible + await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name); + + // # Send a message after returning from background + const afterMessage = `After background ${getRandomId()}`; + await ChannelScreen.postMessage(afterMessage); + + // * Allow time for sync under degraded conditions + await wait(timeouts.TEN_SEC * timeoutMultiplier); + + // * Verify message was sent + const {post: afterPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); + expect(afterPost.message).toBe(afterMessage); + + // # Go back to channel list screen + await ChannelScreen.back(); + }); + + it('DDIL-T011 - should receive messages sent while app was backgrounded', async () => { + // # Open a channel screen + await ChannelScreen.open(channelsCategory, testChannel.name); + + // # Background the app + await device.sendToBackground(); + + // # Send a message via API while app is backgrounded + const apiMessage = `API message while backgrounded ${getRandomId()}`; + await Post.apiCreatePost(siteOneUrl, { + channel_id: testChannel.id, + message: apiMessage, + }); + + // # Wait a bit then bring app back + await wait(timeouts.FOUR_SEC); + await device.launchApp({newInstance: false}); + + // * Allow time for sync under degraded conditions + await wait(timeouts.TEN_SEC * timeoutMultiplier); + + // * Verify the message appears in the channel + const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); + const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, apiMessage); + + await waitFor(postListPostItem) + .toBeVisible() + .withTimeout(timeouts.ONE_MIN * timeoutMultiplier); + + // # Go back to channel list screen + await ChannelScreen.back(); + }); + + it('DDIL-T012 - should handle rapid navigation under degraded network', async () => { + // Rapid navigation tests the app's ability to cancel pending requests + // and handle state transitions under slow network + + // # Navigate into channel + await ChannelScreen.open(channelsCategory, testChannel.name); + await expect(ChannelScreen.headerTitle).toBeVisible(); + + // # Quick back navigation + await ChannelScreen.back(); + await ChannelListScreen.toBeVisible(); + + // # Navigate back into channel + await ChannelScreen.open(channelsCategory, testChannel.name); + + // # Try to type and send a message + const message = `Rapid nav test ${getRandomId()}`; + await ChannelScreen.postMessage(message); + + // * Verify message sent successfully + await wait(timeouts.TEN_SEC * timeoutMultiplier); + const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); + expect(post.message).toBe(message); + + // # Go back + await ChannelScreen.back(); + }); + + it('DDIL-T013 - should show connection status indicator appropriately', async () => { + // This test validates that the app shows appropriate UI feedback + // about connection state under degraded conditions + + // Note: The specific testIDs for connection banner may vary + // Update these based on actual component implementation + + // # Open a channel screen + await ChannelScreen.open(channelsCategory, testChannel.name); + + // * Under degraded conditions, the app should still be functional + // even if showing slow connection indicators + + // # Send a message to verify functionality + const message = `Connection test ${getRandomId()} - ${getCurrentNetworkProfile()}`; + await ChannelScreen.postMessage(message); + + // * Allow extended time for delivery under degraded network + await wait(timeouts.HALF_MIN * timeoutMultiplier); + + // * Verify message was delivered + const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); + expect(post.message).toBe(message); + + // # Go back to channel list screen + await ChannelScreen.back(); + }); +}); diff --git a/detox/e2e/test/degradation/message_delivery.e2e.ts b/detox/e2e/test/degradation/message_delivery.e2e.ts new file mode 100644 index 0000000000..7b3731185b --- /dev/null +++ b/detox/e2e/test/degradation/message_delivery.e2e.ts @@ -0,0 +1,189 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// ******************************************************************* +// - [#] indicates a test step (e.g. # Go to a screen) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element testID when selecting an element. Create one if none. +// ******************************************************************* + +import { + Post, + Setup, +} from '@support/server_api'; +import { + serverOneUrl, + siteOneUrl, +} from '@support/test_config'; +import { + ChannelScreen, + ChannelListScreen, + HomeScreen, + LoginScreen, + ServerScreen, +} from '@support/ui/screen'; +import { + getRandomId, + timeouts, + wait, + getCurrentNetworkProfile, + getTimeoutMultiplier, + getNetworkProfileInfo, + isDegradedNetwork, +} from '@support/utils'; +import {expect} from 'detox'; + +describe('Degradation - Message Delivery', () => { + const serverOneDisplayName = 'Server 1'; + const channelsCategory = 'channels'; + let testChannel: any; + let testUser: any; + const timeoutMultiplier = getTimeoutMultiplier(); + + beforeAll(async () => { + // Log network profile for debugging - visible in test output + // eslint-disable-next-line no-console + console.log(getNetworkProfileInfo()); + + const {channel, user} = await Setup.apiInit(siteOneUrl); + testChannel = channel; + testUser = user; + + // # Log in to server + await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); + await LoginScreen.login(user); + }); + + beforeEach(async () => { + // * Verify on channel list screen + await ChannelListScreen.toBeVisible(); + }); + + afterAll(async () => { + // # Log out + await HomeScreen.logout(); + }); + + it('DDIL-T001 - should eventually deliver message under degraded network', async () => { + // # Open a channel screen + await ChannelScreen.open(channelsCategory, testChannel.name); + + // # Create and send a message + const message = `Degradation test ${getRandomId()} - ${getCurrentNetworkProfile()}`; + await ChannelScreen.postInput.tap(); + await ChannelScreen.postInput.replaceText(message); + + // # Tap send button + await ChannelScreen.sendButton.tap(); + + // * Wait for message to appear in post list (with extended timeout for degraded network) + // Under degraded conditions, this may take significantly longer + await waitFor(ChannelScreen.postInput) + .not.toHaveValue(message) + .withTimeout(timeouts.ONE_MIN * timeoutMultiplier); + + // * Verify message eventually appears (allow time for server round-trip) + await wait(timeouts.TEN_SEC * timeoutMultiplier); + + const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); + expect(post.message).toBe(message); + + // * Verify the message is visible in the UI + const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, message); + await expect(postListPostItem).toBeVisible(); + + // # Go back to channel list screen + await ChannelScreen.back(); + }); + + it('DDIL-T002 - should show loading state for slow message send', async () => { + // Skip this test under normal network conditions as it requires degraded network + if (!isDegradedNetwork()) { + console.log('Skipping test - not running under degraded network'); + return; + } + + // # Open a channel screen + await ChannelScreen.open(channelsCategory, testChannel.name); + + // # Create a message + const message = `Slow send test ${getRandomId()}`; + await ChannelScreen.postInput.tap(); + await ChannelScreen.postInput.replaceText(message); + + // # Tap send button + await ChannelScreen.sendButton.tap(); + + // * Under degraded network, the send button should be disabled while sending + // This validates the app provides appropriate feedback during slow operations + await expect(ChannelScreen.sendButtonDisabled).toBeVisible(); + + // * Wait for message to complete sending + await waitFor(ChannelScreen.sendButtonDisabled) + .toBeVisible() + .withTimeout(timeouts.TWO_MIN * timeoutMultiplier); + + // * Verify message was delivered + const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); + expect(post.message).toBe(message); + + // # Go back to channel list screen + await ChannelScreen.back(); + }); + + it('DDIL-T003 - should send multiple messages in order', async () => { + // # Open a channel screen + await ChannelScreen.open(channelsCategory, testChannel.name); + + const messages = [ + `First message ${getRandomId()}`, + `Second message ${getRandomId()}`, + `Third message ${getRandomId()}`, + ]; + + // # Send multiple messages in sequence + for (const message of messages) { + await ChannelScreen.postMessage(message); + // Small delay between messages to ensure order + await wait(timeouts.ONE_SEC); + } + + // * Allow time for all messages to sync + await wait(timeouts.TEN_SEC * timeoutMultiplier); + + // * Verify all messages arrived in order via API + const response = await Post.apiGetPostsInChannel(siteOneUrl, testChannel.id); + const recentPosts = response.order + .slice(0, 3) + .map((id: string) => response.posts[id].message); + + // Posts are ordered newest-first, so reverse for chronological order + const chronologicalPosts = recentPosts.reverse(); + + for (let i = 0; i < messages.length; i++) { + expect(chronologicalPosts[i]).toBe(messages[i]); + } + + // # Go back to channel list screen + await ChannelScreen.back(); + }); + + it('DDIL-T004 - should handle channel switch during slow network', async () => { + // # Open first channel + await ChannelScreen.open(channelsCategory, testChannel.name); + + // * Verify channel loads + await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name); + + // # Go back and verify channel list is responsive + await ChannelScreen.back(); + await ChannelListScreen.toBeVisible(); + + // # Open channel again - should work even under degradation + await ChannelScreen.open(channelsCategory, testChannel.name); + await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name); + + // # Go back + await ChannelScreen.back(); + }); +}); From 4b71ddb5f00f4c6d03b382f073f9926f27965f13 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 17:11:12 +0200 Subject: [PATCH 07/12] Fix lint errors in degradation tests - Remove unused testUser variables - Fix dot-location lint errors (chain methods on same line) - Add eslint-disable for intentional console.log and await-in-loop Made-with: Cursor --- .../test/degradation/connection_recovery.e2e.ts | 6 +----- .../test/degradation/message_delivery.e2e.ts | 17 +++++------------ 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/detox/e2e/test/degradation/connection_recovery.e2e.ts b/detox/e2e/test/degradation/connection_recovery.e2e.ts index 6314c1a16e..21aa8391d1 100644 --- a/detox/e2e/test/degradation/connection_recovery.e2e.ts +++ b/detox/e2e/test/degradation/connection_recovery.e2e.ts @@ -36,7 +36,6 @@ describe('Degradation - Connection Recovery', () => { const serverOneDisplayName = 'Server 1'; const channelsCategory = 'channels'; let testChannel: any; - let testUser: any; const timeoutMultiplier = getTimeoutMultiplier(); beforeAll(async () => { @@ -46,7 +45,6 @@ describe('Degradation - Connection Recovery', () => { const {channel, user} = await Setup.apiInit(siteOneUrl); testChannel = channel; - testUser = user; // # Log in to server await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); @@ -125,9 +123,7 @@ describe('Degradation - Connection Recovery', () => { const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, apiMessage); - await waitFor(postListPostItem) - .toBeVisible() - .withTimeout(timeouts.ONE_MIN * timeoutMultiplier); + await waitFor(postListPostItem).toBeVisible().withTimeout(timeouts.ONE_MIN * timeoutMultiplier); // # Go back to channel list screen await ChannelScreen.back(); diff --git a/detox/e2e/test/degradation/message_delivery.e2e.ts b/detox/e2e/test/degradation/message_delivery.e2e.ts index 7b3731185b..25cb187dcd 100644 --- a/detox/e2e/test/degradation/message_delivery.e2e.ts +++ b/detox/e2e/test/degradation/message_delivery.e2e.ts @@ -37,7 +37,6 @@ describe('Degradation - Message Delivery', () => { const serverOneDisplayName = 'Server 1'; const channelsCategory = 'channels'; let testChannel: any; - let testUser: any; const timeoutMultiplier = getTimeoutMultiplier(); beforeAll(async () => { @@ -47,7 +46,6 @@ describe('Degradation - Message Delivery', () => { const {channel, user} = await Setup.apiInit(siteOneUrl); testChannel = channel; - testUser = user; // # Log in to server await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); @@ -78,9 +76,7 @@ describe('Degradation - Message Delivery', () => { // * Wait for message to appear in post list (with extended timeout for degraded network) // Under degraded conditions, this may take significantly longer - await waitFor(ChannelScreen.postInput) - .not.toHaveValue(message) - .withTimeout(timeouts.ONE_MIN * timeoutMultiplier); + await waitFor(ChannelScreen.postInput).not.toHaveValue(message).withTimeout(timeouts.ONE_MIN * timeoutMultiplier); // * Verify message eventually appears (allow time for server round-trip) await wait(timeouts.TEN_SEC * timeoutMultiplier); @@ -99,6 +95,7 @@ describe('Degradation - Message Delivery', () => { it('DDIL-T002 - should show loading state for slow message send', async () => { // Skip this test under normal network conditions as it requires degraded network if (!isDegradedNetwork()) { + // eslint-disable-next-line no-console console.log('Skipping test - not running under degraded network'); return; } @@ -119,9 +116,7 @@ describe('Degradation - Message Delivery', () => { await expect(ChannelScreen.sendButtonDisabled).toBeVisible(); // * Wait for message to complete sending - await waitFor(ChannelScreen.sendButtonDisabled) - .toBeVisible() - .withTimeout(timeouts.TWO_MIN * timeoutMultiplier); + await waitFor(ChannelScreen.sendButtonDisabled).toBeVisible().withTimeout(timeouts.TWO_MIN * timeoutMultiplier); // * Verify message was delivered const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -142,9 +137,9 @@ describe('Degradation - Message Delivery', () => { ]; // # Send multiple messages in sequence + // eslint-disable-next-line no-await-in-loop for (const message of messages) { await ChannelScreen.postMessage(message); - // Small delay between messages to ensure order await wait(timeouts.ONE_SEC); } @@ -153,9 +148,7 @@ describe('Degradation - Message Delivery', () => { // * Verify all messages arrived in order via API const response = await Post.apiGetPostsInChannel(siteOneUrl, testChannel.id); - const recentPosts = response.order - .slice(0, 3) - .map((id: string) => response.posts[id].message); + const recentPosts = response.order.slice(0, 3).map((id: string) => response.posts[id].message); // Posts are ordered newest-first, so reverse for chronological order const chronologicalPosts = recentPosts.reverse(); From 61258cd8f2a92559921b217fffc01bf97549181c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 17:18:31 +0200 Subject: [PATCH 08/12] Address PR review feedback from CodeRabbitAI - Move import to top of file in index.ts - Remove unused test_filter input from iOS/Android templates - Use read -r in nightly workflow to prevent backslash mangling - Add cleanup traps to flapping scripts for graceful shutdown - Detect network interface dynamically in Linux flapping script - Fix DDIL-T002 test to wait for send completion instead of button state - Rename DDIL-T013 test to accurately describe what it tests - Fix eslint-disable placement for no-await-in-loop - Add documentation comment about flapping pattern being docs-only - Document LOW_BANDWIDTH_MODE mapping rationale Made-with: Cursor --- .../flapping-linux.sh | 23 +++++++++++++++---- .../actions/bandwidth-throttling/flapping.sh | 16 ++++++++----- .../actions/bandwidth-throttling/profiles.yml | 3 ++- .github/workflows/e2e-android-template.yml | 5 ---- .github/workflows/e2e-degradation-nightly.yml | 2 +- .github/workflows/e2e-ios-template.yml | 5 ---- detox/e2e/support/utils/index.ts | 4 ++-- detox/e2e/support/utils/network_profiles.ts | 2 ++ .../degradation/connection_recovery.e2e.ts | 15 ++++-------- .../test/degradation/message_delivery.e2e.ts | 9 ++++---- 10 files changed, 45 insertions(+), 39 deletions(-) diff --git a/.github/actions/bandwidth-throttling-linux/flapping-linux.sh b/.github/actions/bandwidth-throttling-linux/flapping-linux.sh index 7a06bd227a..3a2c2f9b5f 100755 --- a/.github/actions/bandwidth-throttling-linux/flapping-linux.sh +++ b/.github/actions/bandwidth-throttling-linux/flapping-linux.sh @@ -4,9 +4,24 @@ set -e +# Detect the primary network interface +INTERFACE=$(ip route | grep default | awk '{print $5}' | head -n1) +if [ -z "$INTERFACE" ]; then + INTERFACE="eth0" +fi + echo "Starting flapping network simulation (Linux)" +echo "Using network interface: $INTERFACE" echo "PID: $$" +# Cleanup function to reset network on exit +cleanup() { + echo "[$(date '+%H:%M:%S')] Cleaning up tc rules..." + sudo tc qdisc del dev "$INTERFACE" root 2>/dev/null || true + exit 0 +} +trap cleanup SIGTERM SIGINT EXIT + # Function to apply network settings using tc/netem apply_settings() { local download=$1 @@ -17,11 +32,11 @@ apply_settings() { echo "[$(date '+%H:%M:%S')] Switching to state: $state_name" # Remove existing rules - sudo tc qdisc del dev eth0 root 2>/dev/null || true + sudo tc qdisc del dev "$INTERFACE" root 2>/dev/null || true if [ "$state_name" = "disconnected" ]; then # 100% packet loss = disconnected - sudo tc qdisc add dev eth0 root netem loss 100% + sudo tc qdisc add dev "$INTERFACE" root netem loss 100% else # Build netem options NETEM_OPTS="delay ${latency}ms" @@ -30,12 +45,12 @@ apply_settings() { fi # Add netem for delay/loss - sudo tc qdisc add dev eth0 root handle 1: netem $NETEM_OPTS + sudo tc qdisc add dev "$INTERFACE" root handle 1: netem $NETEM_OPTS # Add rate limiting RATE="${download}kbit" BURST="$((download / 8))kb" - sudo tc qdisc add dev eth0 parent 1: handle 2: tbf rate $RATE burst $BURST latency 50ms + sudo tc qdisc add dev "$INTERFACE" parent 1: handle 2: tbf rate $RATE burst $BURST latency 50ms fi } diff --git a/.github/actions/bandwidth-throttling/flapping.sh b/.github/actions/bandwidth-throttling/flapping.sh index 54bf0c5502..89c5322d8e 100755 --- a/.github/actions/bandwidth-throttling/flapping.sh +++ b/.github/actions/bandwidth-throttling/flapping.sh @@ -7,16 +7,20 @@ set -e -TEST_SERVER_HOST="$1" - -if [ -z "$TEST_SERVER_HOST" ]; then - echo "Error: test_server_host is required" - exit 1 -fi +# TEST_SERVER_HOST is kept for logging/diagnostic purposes only +TEST_SERVER_HOST="${1:-unspecified}" echo "Starting flapping network simulation for host: $TEST_SERVER_HOST" echo "PID: $$" +# Cleanup function to reset network on exit +cleanup() { + echo "[$(date '+%H:%M:%S')] Cleaning up dnctl pipes..." + sudo dnctl -q pipe flush 2>/dev/null || true + exit 0 +} +trap cleanup SIGTERM SIGINT EXIT + # Function to apply network settings apply_settings() { local download=$1 diff --git a/.github/actions/bandwidth-throttling/profiles.yml b/.github/actions/bandwidth-throttling/profiles.yml index edf6ecff7d..c4702f39ab 100644 --- a/.github/actions/bandwidth-throttling/profiles.yml +++ b/.github/actions/bandwidth-throttling/profiles.yml @@ -54,7 +54,8 @@ profiles: upload_kbps: 256 latency_ms: 200 packet_loss_percent: 0 - # Flapping pattern (handled by flapping.sh script) + # NOTE: This pattern is documentation only - actual behavior is hardcoded in + # flapping.sh (macOS) and flapping-linux.sh. Update those scripts to change timing. pattern: - state: "connected" duration_sec: 30 diff --git a/.github/workflows/e2e-android-template.yml b/.github/workflows/e2e-android-template.yml index 81371ad273..ebbd340fd2 100644 --- a/.github/workflows/e2e-android-template.yml +++ b/.github/workflows/e2e-android-template.yml @@ -48,11 +48,6 @@ on: required: false type: string default: "" - test_filter: - description: "Test filter pattern (e.g., 'degradation' to run only degradation tests)" - required: false - type: string - default: "" android_avd_name: description: "Android Emulator name" required: false diff --git a/.github/workflows/e2e-degradation-nightly.yml b/.github/workflows/e2e-degradation-nightly.yml index be42381773..9d2efb2793 100644 --- a/.github/workflows/e2e-degradation-nightly.yml +++ b/.github/workflows/e2e-degradation-nightly.yml @@ -187,7 +187,7 @@ jobs: echo "# DDIL/Degradation Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Network Profiles Tested" >> $GITHUB_STEP_SUMMARY - echo '${{ needs.setup-matrix.outputs.profiles }}' | jq -r '.[]' | while read profile; do + echo '${{ needs.setup-matrix.outputs.profiles }}' | jq -r '.[]' | while read -r profile; do echo "- $profile" >> $GITHUB_STEP_SUMMARY done echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/e2e-ios-template.yml b/.github/workflows/e2e-ios-template.yml index c0470ceb07..ec7e818ca0 100644 --- a/.github/workflows/e2e-ios-template.yml +++ b/.github/workflows/e2e-ios-template.yml @@ -54,11 +54,6 @@ on: required: false type: string default: "" - test_filter: - description: "Test filter pattern (e.g., 'degradation' to run only degradation tests)" - required: false - type: string - default: "" outputs: STATUS: value: ${{ jobs.generate-report.outputs.STATUS }} diff --git a/detox/e2e/support/utils/index.ts b/detox/e2e/support/utils/index.ts index 8f1db4f5f3..1f6ebeb49a 100644 --- a/detox/e2e/support/utils/index.ts +++ b/detox/e2e/support/utils/index.ts @@ -4,6 +4,8 @@ import {adminEmail, adminPassword, adminUsername} from '@support/test_config'; import {v4 as uuidv4} from 'uuid'; +import {getTimeoutMultiplier} from './network_profiles'; + export * from './email'; export * from './detoxhelpers'; export * from './network_profiles'; @@ -64,8 +66,6 @@ export const getAdminAccount = () => { }; }; -import {getTimeoutMultiplier} from './network_profiles'; - // Get timeout multiplier from network profile (supports both new NETWORK_PROFILE and legacy LOW_BANDWIDTH_MODE) const TIMEOUT_MULTIPLIER = getTimeoutMultiplier(); const SECOND = 1000 * TIMEOUT_MULTIPLIER; diff --git a/detox/e2e/support/utils/network_profiles.ts b/detox/e2e/support/utils/network_profiles.ts index 4921958b55..42ed3eb372 100644 --- a/detox/e2e/support/utils/network_profiles.ts +++ b/detox/e2e/support/utils/network_profiles.ts @@ -86,6 +86,8 @@ export function getCurrentNetworkProfile(): NetworkProfile { } // Backward compatibility with LOW_BANDWIDTH_MODE + // Legacy mode uses 3300/3300/500 settings from prepare-low-bandwidth action + // Map to slow_3g for timeout multiplier (3x) - close enough for test timing purposes if (process.env.LOW_BANDWIDTH_MODE === 'true') { return 'slow_3g'; } diff --git a/detox/e2e/test/degradation/connection_recovery.e2e.ts b/detox/e2e/test/degradation/connection_recovery.e2e.ts index 21aa8391d1..4419205fc0 100644 --- a/detox/e2e/test/degradation/connection_recovery.e2e.ts +++ b/detox/e2e/test/degradation/connection_recovery.e2e.ts @@ -157,21 +157,14 @@ describe('Degradation - Connection Recovery', () => { await ChannelScreen.back(); }); - it('DDIL-T013 - should show connection status indicator appropriately', async () => { - // This test validates that the app shows appropriate UI feedback - // about connection state under degraded conditions - - // Note: The specific testIDs for connection banner may vary - // Update these based on actual component implementation + it('DDIL-T013 - should deliver messages under extended degraded conditions', async () => { + // This test validates basic message delivery under prolonged degraded conditions // # Open a channel screen await ChannelScreen.open(channelsCategory, testChannel.name); - // * Under degraded conditions, the app should still be functional - // even if showing slow connection indicators - - // # Send a message to verify functionality - const message = `Connection test ${getRandomId()} - ${getCurrentNetworkProfile()}`; + // # Send a message to verify functionality under degraded network + const message = `Extended degradation test ${getRandomId()} - ${getCurrentNetworkProfile()}`; await ChannelScreen.postMessage(message); // * Allow extended time for delivery under degraded network diff --git a/detox/e2e/test/degradation/message_delivery.e2e.ts b/detox/e2e/test/degradation/message_delivery.e2e.ts index 25cb187dcd..74689e70e4 100644 --- a/detox/e2e/test/degradation/message_delivery.e2e.ts +++ b/detox/e2e/test/degradation/message_delivery.e2e.ts @@ -115,8 +115,8 @@ describe('Degradation - Message Delivery', () => { // This validates the app provides appropriate feedback during slow operations await expect(ChannelScreen.sendButtonDisabled).toBeVisible(); - // * Wait for message to complete sending - await waitFor(ChannelScreen.sendButtonDisabled).toBeVisible().withTimeout(timeouts.TWO_MIN * timeoutMultiplier); + // * Wait for message to complete sending (input clears when sent) + await waitFor(ChannelScreen.postInput).toHaveText('').withTimeout(timeouts.TWO_MIN * timeoutMultiplier); // * Verify message was delivered const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -136,10 +136,11 @@ describe('Degradation - Message Delivery', () => { `Third message ${getRandomId()}`, ]; - // # Send multiple messages in sequence - // eslint-disable-next-line no-await-in-loop + // # Send multiple messages in sequence (sequential await is intentional for test ordering) for (const message of messages) { + // eslint-disable-next-line no-await-in-loop await ChannelScreen.postMessage(message); + // eslint-disable-next-line no-await-in-loop await wait(timeouts.ONE_SEC); } From fc5482879a37d95e52fb83ba1b3646f8605776c7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 17:20:32 +0200 Subject: [PATCH 09/12] Cap extreme timeouts and improve test data isolation - Add cappedTimeout helper to prevent unreasonably long waits under edge_2g (e.g., TWO_MIN * 10 = 20 minutes is capped to 3 minutes max) - Fix DDIL-T003 test data isolation by filtering posts by message content instead of assuming the latest 3 posts are ours Made-with: Cursor --- .../degradation/connection_recovery.e2e.ts | 15 ++++++++---- .../test/degradation/message_delivery.e2e.ts | 24 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/detox/e2e/test/degradation/connection_recovery.e2e.ts b/detox/e2e/test/degradation/connection_recovery.e2e.ts index 4419205fc0..d353522990 100644 --- a/detox/e2e/test/degradation/connection_recovery.e2e.ts +++ b/detox/e2e/test/degradation/connection_recovery.e2e.ts @@ -32,6 +32,11 @@ import { } from '@support/utils'; import {expect} from 'detox'; +// Cap scaled timeouts to prevent extremely long waits (max 3 minutes) +const MAX_SCALED_TIMEOUT = 3 * 60 * 1000; +const cappedTimeout = (baseTimeout: number, multiplier: number) => + Math.min(baseTimeout * multiplier, MAX_SCALED_TIMEOUT); + describe('Degradation - Connection Recovery', () => { const serverOneDisplayName = 'Server 1'; const channelsCategory = 'channels'; @@ -88,7 +93,7 @@ describe('Degradation - Connection Recovery', () => { await ChannelScreen.postMessage(afterMessage); // * Allow time for sync under degraded conditions - await wait(timeouts.TEN_SEC * timeoutMultiplier); + await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); // * Verify message was sent const {post: afterPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -117,13 +122,13 @@ describe('Degradation - Connection Recovery', () => { await device.launchApp({newInstance: false}); // * Allow time for sync under degraded conditions - await wait(timeouts.TEN_SEC * timeoutMultiplier); + await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); // * Verify the message appears in the channel const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, apiMessage); - await waitFor(postListPostItem).toBeVisible().withTimeout(timeouts.ONE_MIN * timeoutMultiplier); + await waitFor(postListPostItem).toBeVisible().withTimeout(cappedTimeout(timeouts.ONE_MIN, timeoutMultiplier)); // # Go back to channel list screen await ChannelScreen.back(); @@ -149,7 +154,7 @@ describe('Degradation - Connection Recovery', () => { await ChannelScreen.postMessage(message); // * Verify message sent successfully - await wait(timeouts.TEN_SEC * timeoutMultiplier); + await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); expect(post.message).toBe(message); @@ -168,7 +173,7 @@ describe('Degradation - Connection Recovery', () => { await ChannelScreen.postMessage(message); // * Allow extended time for delivery under degraded network - await wait(timeouts.HALF_MIN * timeoutMultiplier); + await wait(cappedTimeout(timeouts.HALF_MIN, timeoutMultiplier)); // * Verify message was delivered const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); diff --git a/detox/e2e/test/degradation/message_delivery.e2e.ts b/detox/e2e/test/degradation/message_delivery.e2e.ts index 74689e70e4..db29e23d09 100644 --- a/detox/e2e/test/degradation/message_delivery.e2e.ts +++ b/detox/e2e/test/degradation/message_delivery.e2e.ts @@ -33,6 +33,11 @@ import { } from '@support/utils'; import {expect} from 'detox'; +// Cap scaled timeouts to prevent extremely long waits (max 3 minutes) +const MAX_SCALED_TIMEOUT = 3 * 60 * 1000; +const cappedTimeout = (baseTimeout: number, multiplier: number) => + Math.min(baseTimeout * multiplier, MAX_SCALED_TIMEOUT); + describe('Degradation - Message Delivery', () => { const serverOneDisplayName = 'Server 1'; const channelsCategory = 'channels'; @@ -76,10 +81,10 @@ describe('Degradation - Message Delivery', () => { // * Wait for message to appear in post list (with extended timeout for degraded network) // Under degraded conditions, this may take significantly longer - await waitFor(ChannelScreen.postInput).not.toHaveValue(message).withTimeout(timeouts.ONE_MIN * timeoutMultiplier); + await waitFor(ChannelScreen.postInput).not.toHaveValue(message).withTimeout(cappedTimeout(timeouts.ONE_MIN, timeoutMultiplier)); // * Verify message eventually appears (allow time for server round-trip) - await wait(timeouts.TEN_SEC * timeoutMultiplier); + await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); expect(post.message).toBe(message); @@ -116,7 +121,7 @@ describe('Degradation - Message Delivery', () => { await expect(ChannelScreen.sendButtonDisabled).toBeVisible(); // * Wait for message to complete sending (input clears when sent) - await waitFor(ChannelScreen.postInput).toHaveText('').withTimeout(timeouts.TWO_MIN * timeoutMultiplier); + await waitFor(ChannelScreen.postInput).toHaveText('').withTimeout(cappedTimeout(timeouts.TWO_MIN, timeoutMultiplier)); // * Verify message was delivered const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -145,14 +150,21 @@ describe('Degradation - Message Delivery', () => { } // * Allow time for all messages to sync - await wait(timeouts.TEN_SEC * timeoutMultiplier); + await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); // * Verify all messages arrived in order via API const response = await Post.apiGetPostsInChannel(siteOneUrl, testChannel.id); - const recentPosts = response.order.slice(0, 3).map((id: string) => response.posts[id].message); + + // Filter to find only our test messages (by matching the unique message content) + const ourPosts = response.order + .map((id: string) => response.posts[id].message) + .filter((msg: string) => messages.some((m) => msg === m)); + + // Verify we found all our messages + expect(ourPosts.length).toBe(messages.length); // Posts are ordered newest-first, so reverse for chronological order - const chronologicalPosts = recentPosts.reverse(); + const chronologicalPosts = ourPosts.reverse(); for (let i = 0; i < messages.length; i++) { expect(chronologicalPosts[i]).toBe(messages[i]); From 110fcdea8ddff31477acc740806337805c06117e Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 17:29:00 +0200 Subject: [PATCH 10/12] Fix DDIL-T003 to use correct API response structure apiGetPostsInChannel returns {posts: Post[]} not {order, posts} map Made-with: Cursor --- detox/e2e/test/degradation/message_delivery.e2e.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/detox/e2e/test/degradation/message_delivery.e2e.ts b/detox/e2e/test/degradation/message_delivery.e2e.ts index db29e23d09..d2ef51c49e 100644 --- a/detox/e2e/test/degradation/message_delivery.e2e.ts +++ b/detox/e2e/test/degradation/message_delivery.e2e.ts @@ -153,11 +153,12 @@ describe('Degradation - Message Delivery', () => { await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); // * Verify all messages arrived in order via API - const response = await Post.apiGetPostsInChannel(siteOneUrl, testChannel.id); + // apiGetPostsInChannel returns {posts: Post[]} where posts is an ordered array (newest first) + const {posts} = await Post.apiGetPostsInChannel(siteOneUrl, testChannel.id); // Filter to find only our test messages (by matching the unique message content) - const ourPosts = response.order - .map((id: string) => response.posts[id].message) + const ourPosts = posts + .map((p: {message: string}) => p.message) .filter((msg: string) => messages.some((m) => msg === m)); // Verify we found all our messages From 4816add06fe0d45a02bf966b80877453073156a7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 17:32:17 +0200 Subject: [PATCH 11/12] Fix dot-location lint errors in message_delivery test Made-with: Cursor --- detox/e2e/test/degradation/message_delivery.e2e.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detox/e2e/test/degradation/message_delivery.e2e.ts b/detox/e2e/test/degradation/message_delivery.e2e.ts index d2ef51c49e..536e21de4e 100644 --- a/detox/e2e/test/degradation/message_delivery.e2e.ts +++ b/detox/e2e/test/degradation/message_delivery.e2e.ts @@ -157,9 +157,9 @@ describe('Degradation - Message Delivery', () => { const {posts} = await Post.apiGetPostsInChannel(siteOneUrl, testChannel.id); // Filter to find only our test messages (by matching the unique message content) - const ourPosts = posts - .map((p: {message: string}) => p.message) - .filter((msg: string) => messages.some((m) => msg === m)); + const ourPosts = posts. + map((p: {message: string}) => p.message). + filter((msg: string) => messages.some((m) => msg === m)); // Verify we found all our messages expect(ourPosts.length).toBe(messages.length); From 956cb48e9df7db309faa0f9a561d45891ed9ea09 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Mar 2026 18:27:45 +0200 Subject: [PATCH 12/12] Fix double timeout scaling and improve test patterns - Remove redundant timeout multiplier from cappedTimeout (timeouts are already scaled in index.ts via getTimeoutMultiplier()) - Use it.skip pattern instead of early return for conditional tests - Fix flapping cleanup to preserve exit status instead of always returning 0 - Remove unused getTimeoutMultiplier imports Made-with: Cursor --- .../flapping-linux.sh | 5 ++-- .../degradation/connection_recovery.e2e.ts | 20 ++++++------- .../test/degradation/message_delivery.e2e.ts | 29 +++++++------------ 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/.github/actions/bandwidth-throttling-linux/flapping-linux.sh b/.github/actions/bandwidth-throttling-linux/flapping-linux.sh index 3a2c2f9b5f..d35184de40 100755 --- a/.github/actions/bandwidth-throttling-linux/flapping-linux.sh +++ b/.github/actions/bandwidth-throttling-linux/flapping-linux.sh @@ -14,11 +14,12 @@ echo "Starting flapping network simulation (Linux)" echo "Using network interface: $INTERFACE" echo "PID: $$" -# Cleanup function to reset network on exit +# Cleanup function to reset network on exit (preserves exit status) cleanup() { + local exit_status=$? echo "[$(date '+%H:%M:%S')] Cleaning up tc rules..." sudo tc qdisc del dev "$INTERFACE" root 2>/dev/null || true - exit 0 + exit $exit_status } trap cleanup SIGTERM SIGINT EXIT diff --git a/detox/e2e/test/degradation/connection_recovery.e2e.ts b/detox/e2e/test/degradation/connection_recovery.e2e.ts index d353522990..0efa00c000 100644 --- a/detox/e2e/test/degradation/connection_recovery.e2e.ts +++ b/detox/e2e/test/degradation/connection_recovery.e2e.ts @@ -26,22 +26,20 @@ import { getRandomId, timeouts, wait, - getTimeoutMultiplier, getNetworkProfileInfo, getCurrentNetworkProfile, } from '@support/utils'; import {expect} from 'detox'; -// Cap scaled timeouts to prevent extremely long waits (max 3 minutes) -const MAX_SCALED_TIMEOUT = 3 * 60 * 1000; -const cappedTimeout = (baseTimeout: number, multiplier: number) => - Math.min(baseTimeout * multiplier, MAX_SCALED_TIMEOUT); +// Cap timeouts to prevent extremely long waits (max 3 minutes) +// Note: timeouts.* are already scaled by getTimeoutMultiplier() in index.ts +const MAX_TIMEOUT = 3 * 60 * 1000; +const cappedTimeout = (timeout: number) => Math.min(timeout, MAX_TIMEOUT); describe('Degradation - Connection Recovery', () => { const serverOneDisplayName = 'Server 1'; const channelsCategory = 'channels'; let testChannel: any; - const timeoutMultiplier = getTimeoutMultiplier(); beforeAll(async () => { // Log network profile for debugging - visible in test output @@ -93,7 +91,7 @@ describe('Degradation - Connection Recovery', () => { await ChannelScreen.postMessage(afterMessage); // * Allow time for sync under degraded conditions - await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); + await wait(cappedTimeout(timeouts.TEN_SEC)); // * Verify message was sent const {post: afterPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -122,13 +120,13 @@ describe('Degradation - Connection Recovery', () => { await device.launchApp({newInstance: false}); // * Allow time for sync under degraded conditions - await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); + await wait(cappedTimeout(timeouts.TEN_SEC)); // * Verify the message appears in the channel const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, apiMessage); - await waitFor(postListPostItem).toBeVisible().withTimeout(cappedTimeout(timeouts.ONE_MIN, timeoutMultiplier)); + await waitFor(postListPostItem).toBeVisible().withTimeout(cappedTimeout(timeouts.ONE_MIN)); // # Go back to channel list screen await ChannelScreen.back(); @@ -154,7 +152,7 @@ describe('Degradation - Connection Recovery', () => { await ChannelScreen.postMessage(message); // * Verify message sent successfully - await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); + await wait(cappedTimeout(timeouts.TEN_SEC)); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); expect(post.message).toBe(message); @@ -173,7 +171,7 @@ describe('Degradation - Connection Recovery', () => { await ChannelScreen.postMessage(message); // * Allow extended time for delivery under degraded network - await wait(cappedTimeout(timeouts.HALF_MIN, timeoutMultiplier)); + await wait(cappedTimeout(timeouts.HALF_MIN)); // * Verify message was delivered const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); diff --git a/detox/e2e/test/degradation/message_delivery.e2e.ts b/detox/e2e/test/degradation/message_delivery.e2e.ts index 536e21de4e..51ddcd85e6 100644 --- a/detox/e2e/test/degradation/message_delivery.e2e.ts +++ b/detox/e2e/test/degradation/message_delivery.e2e.ts @@ -27,22 +27,20 @@ import { timeouts, wait, getCurrentNetworkProfile, - getTimeoutMultiplier, getNetworkProfileInfo, isDegradedNetwork, } from '@support/utils'; import {expect} from 'detox'; -// Cap scaled timeouts to prevent extremely long waits (max 3 minutes) -const MAX_SCALED_TIMEOUT = 3 * 60 * 1000; -const cappedTimeout = (baseTimeout: number, multiplier: number) => - Math.min(baseTimeout * multiplier, MAX_SCALED_TIMEOUT); +// Cap timeouts to prevent extremely long waits (max 3 minutes) +// Note: timeouts.* are already scaled by getTimeoutMultiplier() in index.ts +const MAX_TIMEOUT = 3 * 60 * 1000; +const cappedTimeout = (timeout: number) => Math.min(timeout, MAX_TIMEOUT); describe('Degradation - Message Delivery', () => { const serverOneDisplayName = 'Server 1'; const channelsCategory = 'channels'; let testChannel: any; - const timeoutMultiplier = getTimeoutMultiplier(); beforeAll(async () => { // Log network profile for debugging - visible in test output @@ -81,10 +79,10 @@ describe('Degradation - Message Delivery', () => { // * Wait for message to appear in post list (with extended timeout for degraded network) // Under degraded conditions, this may take significantly longer - await waitFor(ChannelScreen.postInput).not.toHaveValue(message).withTimeout(cappedTimeout(timeouts.ONE_MIN, timeoutMultiplier)); + await waitFor(ChannelScreen.postInput).not.toHaveValue(message).withTimeout(cappedTimeout(timeouts.ONE_MIN)); // * Verify message eventually appears (allow time for server round-trip) - await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); + await wait(cappedTimeout(timeouts.TEN_SEC)); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); expect(post.message).toBe(message); @@ -97,14 +95,9 @@ describe('Degradation - Message Delivery', () => { await ChannelScreen.back(); }); - it('DDIL-T002 - should show loading state for slow message send', async () => { - // Skip this test under normal network conditions as it requires degraded network - if (!isDegradedNetwork()) { - // eslint-disable-next-line no-console - console.log('Skipping test - not running under degraded network'); - return; - } - + // This test requires degraded network to observe loading states + const degradedOnlyTest = isDegradedNetwork() ? it : it.skip; + degradedOnlyTest('DDIL-T002 - should show loading state for slow message send', async () => { // # Open a channel screen await ChannelScreen.open(channelsCategory, testChannel.name); @@ -121,7 +114,7 @@ describe('Degradation - Message Delivery', () => { await expect(ChannelScreen.sendButtonDisabled).toBeVisible(); // * Wait for message to complete sending (input clears when sent) - await waitFor(ChannelScreen.postInput).toHaveText('').withTimeout(cappedTimeout(timeouts.TWO_MIN, timeoutMultiplier)); + await waitFor(ChannelScreen.postInput).toHaveText('').withTimeout(cappedTimeout(timeouts.TWO_MIN)); // * Verify message was delivered const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -150,7 +143,7 @@ describe('Degradation - Message Delivery', () => { } // * Allow time for all messages to sync - await wait(cappedTimeout(timeouts.TEN_SEC, timeoutMultiplier)); + await wait(cappedTimeout(timeouts.TEN_SEC)); // * Verify all messages arrived in order via API // apiGetPostsInChannel returns {posts: Post[]} where posts is an ordered array (newest first)