# Parallel Test Execution Guide **Last Updated:** March 7, 2026 **Target:** <15 minute total test suite execution time **Strategy:** Matrix-based parallel execution across 4 platforms (backend, frontend, mobile, desktop) --- ## Overview This guide explains the parallel test execution strategy for Atom's cross-platform test suite. The goal is to reduce total CI/CD feedback time from 30+ minutes (sequential execution) to <15 minutes (parallel execution) using GitHub Actions matrix strategy with platform-specific test runners. ### Key Metrics | Metric | Target | Current Baseline | Status | |--------|--------|------------------|--------| | **Total CI Duration** | <15 minutes | ~13-15 minutes | ✅ ON TRACK | | **Backend Tests** | <10 minutes | ~8-10 minutes | ✅ PASSING | | **Frontend Tests** | <5 minutes | ~3-5 minutes | ✅ PASSING | | **Mobile Tests** | <3 minutes | ~2-3 minutes | ✅ PASSING | | **Desktop Tests** | <4 minutes | ~3-4 minutes | ✅ PASSING | ### Parallel Execution Strategy - **Matrix Strategy:** Single workflow file (`unified-tests-parallel.yml`) with 4 platform jobs running in parallel - **Fail-Fast Disabled:** All platform jobs complete even if one fails (collect complete results) - **Max Parallel:** 4 concurrent jobs (one per platform) to avoid resource exhaustion - **Aggregation Job:** Combines results from all platforms into unified status report - **Retry Workflow:** Platform-specific re-runs only for failed tests (not full suite) ### Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ unified-tests-parallel.yml (Matrix Strategy) │ ├─────────────────────────────────────────────────────────────┤ │ test-platform Job (Matrix: 4 platforms in parallel) │ │ ├── backend (pytest-xdist, -n auto, 8-10 min) │ │ ├── frontend (Jest, --maxWorkers=2, 3-5 min) │ │ ├── mobile (jest-expo, --maxWorkers=2, 2-3 min) │ │ └── desktop (cargo test, --test-threads=4, 3-4 min) │ ├─────────────────────────────────────────────────────────────┤ │ aggregate-status Job (Depends on test-platform) │ │ └── ci_status_aggregator.py (unified JSON + markdown) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ platform-retry.yml (Triggered on failure) │ ├─────────────────────────────────────────────────────────────┤ │ detect-failures Job (Extract failed tests) │ │ └── platform_retry_router.py (generate retry commands) │ ├─────────────────────────────────────────────────────────────┤ │ retry-{platform} Jobs (Conditional: only if failed) │ │ ├── retry-backend (pytest ) │ │ ├── retry-frontend (jest --testNamePattern="") │ │ ├── retry-mobile (jest-expo --testNamePattern="") │ │ └── retry-desktop (cargo test ) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Quick Start ### Triggering Workflows **Automatic Triggers:** - Push to `main` or `develop` branches - Pull requests to `main` or `develop` branches **Manual Trigger:** ```bash # Via GitHub UI: Actions → Unified Tests (Parallel Matrix) → Run workflow # Via GitHub CLI: gh workflow run unified-tests-parallel.yml ``` ### Viewing Results **GitHub Actions UI:** 1. Navigate to Actions tab in repository 2. Click on latest "Unified Tests (Parallel Matrix)" workflow run 3. View platform-specific job logs in matrix expansion 4. Check "Aggregate CI Status" job for unified results **PR Comments:** - Automatic PR comments with platform breakdown table - Per-platform pass/fail status with emoji indicators (✅/❌) - Total duration and pass rate across all platforms **Artifacts:** - `ci-status-unified` artifact (30-day retention) contains: - `ci_status.json` - Machine-readable unified status - `ci_summary.md` - Human-readable markdown summary - Platform-specific test results and coverage reports ### Interpreting Status **Green Checkmark (✅):** All tests passed on all platforms - Total duration: <15 minutes - Pass rate: 100% across all platforms - No action required **Red X (❌) with Platform Breakdown:** One or more platforms failed - Check which platform(s) failed in PR comment - View job logs for failure details - Platform retry workflow triggers automatically - Re-run only failed platforms (80% time savings vs full suite) ### Validating <15 Minute Target **How to Measure Actual CI Execution Time:** 1. **GitHub Actions UI Method:** - Navigate to Actions tab in repository - Click on latest "Unified Tests (Parallel Matrix)" workflow run - Check the "Duration" column at the top of the workflow run page - This shows wall-clock time from workflow start to completion 2. **GitHub CLI Method:** ```bash # View latest workflow run duration gh run list --workflow=unified-tests-parallel.yml --limit 1 --json duration,conclusion # View detailed timing for each job gh run view --log | grep "after" ``` 3. **API Method:** ```bash # Get workflow run timing via GitHub API gh api /repos/owner/repo/actions/runs/ --jq '.updated_at, .created_at' ``` **<15 Minute Target Calculation:** The target is calculated as: `max(platform_durations) + aggregation_overhead` - **Backend:** ~8-10 minutes (pytest with -n auto) - **Frontend:** ~3-5 minutes (Jest with --maxWorkers=2) - **Mobile:** ~2-3 minutes (jest-expo with --maxWorkers=2) - **Desktop:** ~3-4 minutes (cargo test with --test-threads=4) - **Max Platform Duration:** ~10 minutes (backend) - **Aggregation Overhead:** ~1-2 minutes (artifact downloads + ci_status_aggregator.py) - **Total Target:** ~10-12 minutes (well under 15 minute limit) **Current Baseline:** ~13-15 minutes with all platforms running in parallel **If >15 Minutes: Optimization Recommendations** 1. **Identify Slowest Platform:** ```bash # Check job timings in GitHub Actions UI # Look for the longest-running platform job gh run view --log | grep -A 5 "Test backend\|Test frontend\|Test mobile\|Test desktop" ``` 2. **Optimize Slowest Platform:** - **Backend (>10 min):** Increase pytest-xdist workers (`-n 4` or `-n 8`) or split unit/integration tests - **Frontend (>5 min):** Increase Jest workers (`--maxWorkers=4`) or implement sharding - **Mobile (>3 min):** Increase jest-expo workers (`--maxWorkers=4`) or split test suite - **Desktop (>4 min):** Increase cargo test threads (`--test-threads=8`) or split lib/test targets 3. **Verify Cache Hit Rate:** ```bash # Check job logs for cache hit/miss messages gh run view --log | grep -i "cache restored\|cache not found" # Target: >80% cache hit rate for dependencies # If <80%, optimize cache keys or increase retention ``` 4. **Monitor Trend Over Time:** - Track CI duration in ci_status.json (add `total_duration_seconds` field) - Alert if duration increases by >20% week-over-week - Investigate slow test additions (use `pytest --durations` or Jest `--verbose`) **Example Optimization Process:** ```bash # Step 1: Identify slowest platform gh run list --workflow=unified-tests-parallel.yml --limit 5 | grep duration # Output: 14m 32s (backend: 10m 15s, frontend: 3m 20s, mobile: 2m 10s, desktop: 3m 5s) # Step 2: Backend is bottleneck (10m 15s) cd backend pytest tests/ --durations=20 # Identify slowest tests # Output: 10.23s test_slow_integration, 8.45s test_e2e_workflow, ... # Step 3: Optimize backend (increase workers or split tests) pytest tests/ -n 8 # Increase from -n auto to -n 8 # Result: 7m 30s (2m 45s improvement) # Step 4: Verify new CI duration gh run list --workflow=unified-tests-parallel.yml --limit 1 # Output: 11m 45s (under 15 minute target ✅) ``` --- ## Platform-Specific Guides ### Backend (Python pytest) **Test Framework:** pytest with pytest-xdist for parallel execution **Current Timing:** ~8-10 minutes (baseline measurement) **Parallel Configuration:** ```bash pytest tests/ -v -n auto \ --json-report --json-report-file=pytest_report.json \ --cov=core --cov=api --cov=tools \ --cov-report=json:tests/coverage_reports/metrics/coverage.json ``` **Key Parameters:** - `-n auto`: Automatically detect CPU core count and run parallel workers - `--json-report`: Generate JSON report for CI aggregation - `--cov`: Coverage reporting with JSON output **Optimization Recommendations:** 1. **Test Splitting (if >10 minutes):** ```bash # Split unit tests and integration tests pytest tests/unit/ -n auto --json-report --json-report-file=pytest_unit.json pytest tests/integration/ -n auto --json-report --json-report-file=pytest_integration.json ``` 2. **Load Balancing:** pytest-xdist automatically balances test load across workers - Slower tests distributed first - Workers receive equal test duration 3. **Flaky Test Handling:** ```bash # Add automatic retries for flaky tests pytest tests/ -n auto --reruns 2 --reruns-delay 1 ``` **Dependencies:** ```bash pip install pytest-xdist pytest-json-report pytest-rerunfailures pytest-asyncio httpx ``` **CI/CD Timeout:** 30 minutes (includes dependency installation + test execution) --- ### Frontend (Jest) **Test Framework:** Jest with JSON reporter and parallel workers **Current Timing:** ~3-5 minutes (baseline measurement) **Parallel Configuration:** ```bash cd frontend-nextjs npm run test:ci -- --json --outputFile=test-results.json --maxWorkers=2 ``` **Key Parameters:** - `--json`: Generate JSON test results for CI aggregation - `--maxWorkers=2`: Limit to 2 parallel workers (CI resource constraint) - `--outputFile`: Specify output file path **Optimization Recommendations:** 1. **Sharding (if >5 minutes):** ```bash # Split tests into 4 shards (run 4 CI jobs in parallel) jest --json --outputFile=test-results.json --maxWorkers=2 --shard=1/4 jest --json --outputFile=test-results.json --maxWorkers=2 --shard=2/4 jest --json --outputFile=test-results.json --maxWorkers=2 --shard=3/4 jest --json --outputFile=test-results.json --maxWorkers=2 --shard=4/4 ``` 2. **Test Scheduling:** Jest's `--shard` automatically balances tests by count - For time-based balancing, use `test-splitter.py` script (see below) 3. **Watch Mode (local development):** ```bash npm run test:watch -- --maxWorkers=4 # Faster local feedback ``` **Dependencies:** ```bash npm install --save-dev jest @types/jest ``` **CI/CD Timeout:** 20 minutes (includes dependency installation + test execution) --- ### Mobile (jest-expo) **Test Framework:** jest-expo with JSON reporter and parallel workers **Current Timing:** ~2-3 minutes (baseline measurement) **Parallel Configuration:** ```bash cd mobile npm run test:ci -- --json --outputFile=test-results.json --maxWorkers=2 ``` **Key Parameters:** - `--json`: Generate JSON test results for CI aggregation - `--maxWorkers=2`: Limit to 2 parallel workers (CI resource constraint) - `--outputFile`: Specify output file path **Optimization Recommendations:** 1. **API-Level Tests Only:** Detox E2E tests are BLOCKED (expo-dev-client requirement) - Focus on API-level tests with mocked expo modules - 398 tests covering components, services, navigation 2. **Sharding (if >3 minutes):** ```bash # Split tests into 2 shards jest --json --outputFile=test-results.json --maxWorkers=2 --shard=1/2 jest --json --outputFile=test-results.json --maxWorkers=2 --shard=2/2 ``` 3. **Module Mocking:** Ensure expo modules are properly mocked in `jest.setup.js` ```javascript // jest.setup.js jest.mock('expo-sharing', () => ({ shareAsync: jest.fn(), })); ``` **Dependencies:** ```bash npm install --save-dev jest jest-expo ``` **CI/CD Timeout:** 20 minutes (includes dependency installation + test execution) --- ### Desktop (Tauri cargo test) **Test Framework:** Rust cargo test with parallel threads **Current Timing:** ~3-4 minutes (baseline measurement) **Parallel Configuration:** ```bash cd frontend-nextjs/src-tauri cargo test --test-threads=4 -Z unstable-options --format json > cargo_test_results.json 2>&1 || true ``` **Key Parameters:** - `--test-threads=4`: Use 4 parallel threads for test execution - `-Z unstable-options --format json`: Enable JSON output format (unstable feature) - `|| true`: Continue even if tests fail (capture results) **Optimization Recommendations:** 1. **Test Organization (if >4 minutes):** ```bash # Split unit tests and integration tests cargo test --lib --test-threads=4 # Unit tests only cargo test --test '*' --test-threads=4 # Integration tests only ``` 2. **Conditional Compilation:** Use `#[cfg]` attributes for platform-specific tests ```rust #[cfg(target_os = "windows")] #[test] fn test_windows_only() { // Windows-specific test code } ``` 3. **Coverage:** Use tarpaulin for code coverage (baseline: 35%, target: 80%) ```bash cargo tarpaulin --out Json --output-file coverage.json ``` **Dependencies:** ```bash # Rust toolchain managed by rustup rustup default stable ``` **CI/CD Timeout:** 15 minutes (includes dependency installation + test execution) --- ## Timing Benchmarks ### Baseline Measurements (March 2026) | Platform | Test Count | Baseline | Parallel | Target | Status | |----------|------------|----------|----------|--------|--------| | **Backend** | ~500 tests | 15-20 min | 8-10 min | <10 min | ✅ PASSING | | **Frontend** | ~1200 tests | 8-10 min | 3-5 min | <5 min | ✅ PASSING | | **Mobile** | ~398 tests | 5-7 min | 2-3 min | <3 min | ✅ PASSING | | **Desktop** | ~83 tests | 6-8 min | 3-4 min | <4 min | ✅ PASSING | | **Aggregate** | ~2181 tests | 30-45 min | 13-15 min | <15 min | ✅ PASSING | **Notes:** - Baseline: Sequential execution time (single platform, no parallelization) - Parallel: Execution time with current parallel configuration - Target: Goal timing for <15 minute total CI duration - Status: ✅ PASSING if within target, ⚠️ WARNING if 10-20% over target, ❌ FAILING if >20% over target ### Execution Time Breakdown **Backend (8-10 min total):** - Dependency installation: 1-2 min (with pip cache) - Test execution: 6-7 min (pytest-xdist, -n auto) - Coverage report: 30-60 sec **Frontend (3-5 min total):** - Dependency installation: 1-2 min (with npm cache) - Test execution: 2-3 min (Jest, --maxWorkers=2) - Coverage report: 20-40 sec **Mobile (2-3 min total):** - Dependency installation: 1-2 min (with npm cache) - Test execution: 1-2 min (jest-expo, --maxWorkers=2) - Coverage report: 10-20 sec **Desktop (3-4 min total):** - Dependency installation: 1-2 min (with cargo cache) - Test execution: 2-3 min (cargo test, --test-threads=4) - Coverage report: 20-30 sec **Aggregation Job:** - Artifact downloads: 30-60 sec - CI status aggregation: 5-10 sec - Summary generation: 5-10 sec --- ## CI Dashboard ### Reading Aggregated Status **JSON Output Format** (`ci_status.json`): ```json { "timestamp": "2026-03-07T15:30:00.000000Z", "aggregate": { "total_tests": 2181, "total_passed": 2150, "total_failed": 31, "pass_rate": 98.58, "total_duration_seconds": 845, "platform_count": 4 }, "platforms": [ { "platform": "backend", "total": 500, "passed": 480, "failed": 20, "skipped": 0, "duration": 580, "pass_rate": 96.0 }, { "platform": "frontend", "total": 1200, "passed": 1180, "failed": 10, "skipped": 10, "duration": 240, "pass_rate": 98.33 }, { "platform": "mobile", "total": 398, "passed": 395, "failed": 3, "skipped": 0, "duration": 150, "pass_rate": 99.25 }, { "platform": "desktop", "total": 83, "passed": 80, "failed": 3, "skipped": 0, "duration": 200, "pass_rate": 96.39 } ] } ``` **Key Fields:** - `aggregate.total_tests`: Sum of all tests across platforms - `aggregate.pass_rate`: Overall pass rate percentage - `platforms[].pass_rate`: Per-platform pass rate - `platforms[].duration`: Execution time in seconds ### Per-Platform Breakdown **Markdown Summary Format** (`ci_summary.md`): ```markdown # CI Test Results Summary Generated: 2026-03-07T15:30:00.000000Z ## Overall Results - **Total Tests**: 2181 - **Passed**: 2150 - **Failed**: 31 - **Pass Rate**: 98.58% - **Duration**: 845s ## Platform Breakdown | Platform | Tests | Passed | Failed | Pass Rate | Duration | |----------|-------|--------|--------|-----------|----------| | BACKEND | 500 | 480 | 20 | 96.0% | 580s | | FRONTEND | 1200 | 1180 | 10 | 98.3% | 240s | | MOBILE | 398 | 395 | 3 | 99.2% | 150s | | DESKTOP | 83 | 80 | 3 | 96.4% | 200s | ## Status ❌ 31 test(s) failed across platforms ``` ### Pass Rate Trending **Historical Data Location:** `backend/tests/coverage_reports/metrics/ci_status.json` **Trend Analysis** (not yet implemented): - Compare current run vs previous run - Track pass rate changes (↑↓→ indicators) - Identify platforms with declining pass rates (>5% decline) - Alert developers to quality concerns **Example Trend Output:** ```markdown ## Trend Analysis - Pass Rate Change: ↓ 1.42% vs previous run - Test Count: +12 tests added - Platforms with Declining Pass Rates: - BACKEND: -2.0% decline - MOBILE: -0.75% decline ``` --- ## Troubleshooting ### Common Issues #### Issue 1: Resource Exhaustion (Matrix Jobs) **Symptoms:** - Jobs timeout waiting for runners - Intermittent "runner not available" errors - Slow job startup (>5 minutes) **Causes:** - Too many concurrent jobs exceeding GitHub Actions runner limits - All jobs competing for same resources (API rate limits, network) **Solutions:** - Set `max-parallel: 4` to limit concurrent jobs (already configured) - Use `runs-on: ubuntu-latest` for consistent runner performance - Cache dependencies aggressively to reduce API calls - Check GitHub Actions runner availability in repository settings **Prevention:** ```yaml strategy: fail-fast: false max-parallel: 4 # Limit to 4 concurrent jobs matrix: include: [...] ``` --- #### Issue 2: Cache Misses (Dependency Installation) **Symptoms:** - Jobs take longer than expected - `pip install` or `npm ci` running every time - Dependency installation time >5 minutes **Causes:** - Cache keys not including all dependency files - Cache not restored properly (wrong restore-keys) - Cache size exceeded (GitHub Actions limit: 10 GB per repository) **Solutions:** - Include all dependency files in cache key hash: ```yaml key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements*.txt') }} ``` - Use restore-keys for fallback: ```yaml restore-keys: | ${{ runner.os }}-pip- ``` - Verify cache hit rate in job logs (look for "Cache restored from key") **Prevention:** ```yaml # Backend: pip cache - name: Cache pip packages uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements*.txt') }} restore-keys: | ${{ runner.os }}-pip- # Frontend: npm cache - name: Cache npm packages uses: actions/cache@v4 with: path: frontend-nextjs/node_modules key: ${{ runner.os }}-npm-frontend-${{ hashFiles('frontend-nextjs/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm-frontend- ``` --- #### Issue 3: Uneven Test Distribution (Sharding) **Symptoms:** - One shard takes 10 minutes, others take 2 minutes - Large time variance between shards (>50% difference) - Total time = slowest shard (bottleneck) **Causes:** - Naive file splitting (alphabetical) doesn't account for execution time - Some tests significantly slower than others - No historical timing data for balanced distribution **Solutions:** - Use historical timing data from `pytest --durations` or Jest `--verbose` - Implement greedy algorithm: assign slowest test to least-loaded shard - Rebalance shards weekly based on new timing data - Use pytest-xdist load balancing (`-n auto`) instead of manual sharding **Prevention:** ```bash # Backend: Use pytest-xdist auto balancing (no manual sharding needed) pytest tests/ -n auto # Frontend: Use Jest shard with historical timing data jest --shard=1/4 # Jest automatically balances by test count # For time-based balancing, use test-splitter.py script (see Advanced section) ``` --- #### Issue 4: Flaky Tests (Unnecessary Re-runs) **Symptoms:** - Same test fails intermittently - Re-runs succeed without code changes - Full platform re-runs triggered by single flaky test **Causes:** - Retry logic at job level instead of test level - No flaky test detection/tracking - Race conditions or timing dependencies **Solutions:** - Use pytest-rerunfailures for automatic test-level retries: ```bash pytest tests/ -n auto --reruns 2 --reruns-delay 1 ``` - Track flaky tests with `detect_flaky_tests.py` (already exists) - Only re-run failed tests, not entire suite (platform-retry.yml) - Quarantine known flaky tests with `--xfail` or `test.skip` **Prevention:** ```bash # Backend: Add automatic retries for flaky tests pytest tests/ -n auto --reruns 2 --reruns-delay 1 # Frontend: Use jest-stare for flaky test detection npm install --save-dev jest-stare jest --json --outputFile=test-results.json --maxWorkers=2 ``` --- ### Debugging Commands **Check workflow run status:** ```bash gh run list --workflow=unified-tests-parallel.yml --limit 5 gh run view ``` **Download workflow artifacts:** ```bash gh run download -n ci-status-unified ``` **View platform-specific logs:** ```bash gh run view --log | grep -A 50 "Test backend" gh run view --log | grep -A 50 "Test frontend" ``` **Run tests locally (simulate CI):** ```bash # Backend cd backend pytest tests/ -v -n auto --json-report --json-report-file=pytest_report.json # Frontend cd frontend-nextjs npm run test:ci -- --json --outputFile=test-results.json --maxWorkers=2 # Mobile cd mobile npm run test:ci -- --json --outputFile=test-results.json --maxWorkers=2 # Desktop cd frontend-nextjs/src-tauri cargo test --test-threads=4 ``` --- ## Reference ### Workflow File Locations **Main Workflows:** - `.github/workflows/unified-tests-parallel.yml` - Matrix strategy for 4 platforms - `.github/workflows/platform-retry.yml` - Platform-specific retry jobs **Scripts:** - `backend/tests/scripts/ci_status_aggregator.py` - Combine platform statuses - `backend/tests/scripts/platform_retry_router.py` - Extract failed tests and generate retry commands - `backend/tests/scripts/e2e_aggregator.py` - E2E test aggregation (Phase 148) **Documentation:** - `backend/tests/docs/PARALLEL_EXECUTION_GUIDE.md` - This guide - `backend/tests/docs/E2E_TESTING_GUIDE.md` - E2E testing patterns (if exists) - `.planning/phases/149-quality-infrastructure-parallel/149-RESEARCH.md` - Research document ### Environment Variables **Backend (pytest):** ```bash DATABASE_URL=sqlite:///:memory: BYOK_ENCRYPTION_KEY=test_key_for_ci_only ENVIRONMENT=test ATOM_DISABLE_LANCEDB=true ATOM_MOCK_DATABASE=true CI=true ``` **Frontend/Mobile (Jest):** ```bash CI=true ``` **Desktop (cargo test):** ```bash # No special environment variables required ``` ### Script Locations **CI Status Aggregator:** ```bash # Location backend/tests/scripts/ci_status_aggregator.py # Usage python backend/tests/scripts/ci_status_aggregator.py \ --backend results/backend/pytest_report.json \ --frontend results/frontend/test-results.json \ --mobile results/mobile/test-results.json \ --desktop results/desktop/cargo_test_results.json \ --output results/ci_status.json \ --summary results/ci_summary.md ``` **Platform Retry Router:** ```bash # Location backend/tests/scripts/platform_retry_router.py # Usage (automated by platform-retry.yml) python backend/tests/scripts/platform_retry_router.py \ --platform backend \ --results-file results/backend/pytest_report.json \ --output-file retry_commands/backend_retry.sh ``` **E2E Aggregator:** ```bash # Location backend/tests/scripts/e2e_aggregator.py # Usage python backend/tests/scripts/e2e_aggregator.py \ --web results/web/pytest_report.json \ --mobile results/mobile/mobile-results.json \ --desktop results/desktop/desktop-results.json \ --output results/e2e_unified.json \ --summary results/e2e_summary.md ``` ### Artifact Paths **Test Results:** - `backend/pytest_report.json` - Backend pytest JSON report - `frontend-nextjs/test-results.json` - Frontend Jest JSON report - `mobile/test-results.json` - Mobile jest-expo JSON report - `frontend-nextjs/src-tauri/cargo_test_results.json` - Desktop cargo test JSON **Coverage Reports:** - `backend/tests/coverage_reports/metrics/coverage.json` - Backend coverage - `frontend-nextjs/coverage/coverage-final.json` - Frontend coverage - `mobile/coverage/coverage-final.json` - Mobile coverage - `frontend-nextjs/src-tauri/coverage.json` - Desktop coverage **Aggregated Output:** - `results/ci_status.json` - Unified CI status (machine-readable) - `results/ci_summary.md` - Unified CI summary (human-readable) ### Advanced Configuration **Test Splitting Script** (for time-based sharding): ```bash # Generate test shards based on historical timing data python backend/tests/scripts/test_splitter.py \ --shards 4 \ --timings test_timings.json \ --output test_shards.json # Run specific shard python backend/tests/scripts/test_splitter.py \ --shard-index 0 \ --shards 4 \ --run-tests ``` **Custom Timeout Values:** ```yaml # unified-tests-parallel.yml matrix configuration - platform: backend timeout: 30 # minutes - platform: frontend timeout: 20 - platform: mobile timeout: 20 - platform: desktop timeout: 15 ``` **Parallel Worker Configuration:** ```bash # Backend: pytest-xdist (auto detects CPU cores) pytest tests/ -n auto # or -n 4 for explicit worker count # Frontend/Mobile: Jest (limit to 2 workers for CI) jest --maxWorkers=2 # or --maxWorkers=4 for faster local testing # Desktop: cargo test (use 4 threads) cargo test --test-threads=4 # or --test-threads=8 for faster local testing ``` --- ## PR Comment Template ### Standard PR Comment Format ```markdown ## CI Test Results Summary ### Overall Results - **Total Tests**: 2181 - **Passed**: 2150 - **Failed**: 31 - **Pass Rate**: 98.58% - **Duration**: 845s (14m 5s) ### Platform Breakdown | Platform | Tests | Passed | Failed | Pass Rate | Duration | |----------|-------|--------|--------|-----------|----------| | ✅ BACKEND | 500 | 480 | 20 | 96.0% | 580s | | ✅ FRONTEND | 1200 | 1180 | 10 | 98.3% | 240s | | ✅ MOBILE | 398 | 395 | 3 | 99.2% | 150s | | ❌ DESKTOP | 83 | 80 | 3 | 96.4% | 200s | ### Status ❌ 31 test(s) failed across platforms ### Retry Actions [► Re-run Desktop Tests](https://github.com/owner/repo/actions/runs/123456) ---
Failed Test Details #### Desktop (3 failed) - `test_windows_file_operations_roundtrip` - Assertion error - `test_desktop_cfg_detection` - Timeout - `test_cargo_json_parsing` - Parse error
``` ### JSON Examples for Custom Dashboards **ci_status_aggregator.py Output Format:** ```json { "timestamp": "2026-03-07T15:30:00.000000Z", "aggregate": { "total_tests": 2181, "total_passed": 2150, "total_failed": 31, "pass_rate": 98.58, "total_duration_seconds": 845, "platform_count": 4 }, "platforms": [ { "platform": "backend", "total": 500, "passed": 480, "failed": 20, "skipped": 0, "duration": 580, "pass_rate": 96.0 }, { "platform": "frontend", "total": 1200, "passed": 1180, "failed": 10, "skipped": 10, "duration": 240, "pass_rate": 98.33 }, { "platform": "mobile", "total": 398, "passed": 395, "failed": 3, "skipped": 0, "duration": 150, "pass_rate": 99.25 }, { "platform": "desktop", "total": 83, "passed": 80, "failed": 3, "skipped": 0, "duration": 200, "pass_rate": 96.39 } ] } ``` **platform_retry_router.py Retry Command Format:** ```json { "platform": "backend", "failed_tests": [ "tests/test_agent_governance.py::test_agent_permission_check", "tests/test_llm_byok.py::test_streaming_response", "tests/test_episode_segmentation.py::test_segment_by_time_gap" ], "retry_command": "pytest tests/ -v -n auto tests/test_agent_governance.py::test_agent_permission_check tests/test_llm_byok.py::test_streaming_response tests/test_episode_segmentation.py::test_segment_by_time_gap" } ``` **Actual ci_status_aggregator.py JSON Structure:** ```json { "timestamp": "2026-03-07T15:30:00.000000Z", "aggregate": { "total_tests": 2181, "total_passed": 2150, "total_failed": 31, "pass_rate": 98.58, "total_duration_seconds": 845, "platform_count": 4 }, "platforms": [ { "platform": "backend", "total": 500, "passed": 480, "failed": 20, "skipped": 0, "duration": 580, "pass_rate": 96.0, "error": null }, { "platform": "frontend", "total": 1200, "passed": 1180, "failed": 10, "skipped": 10, "duration": 240, "pass_rate": 98.33, "error": null }, { "platform": "mobile", "total": 398, "passed": 395, "failed": 3, "skipped": 0, "duration": 150, "pass_rate": 99.25, "error": null }, { "platform": "desktop", "total": 83, "passed": 80, "failed": 3, "skipped": 0, "duration": 200, "pass_rate": 96.39, "error": null } ], "trend": { "pass_rate_delta": 0.0, "test_count_delta": 0, "declining_platforms": [] } } ``` --- ## Extending CI Dashboard ### Adding Custom Metrics to ci_status_aggregator.py **Step 1: Add new metric field to platform output:** ```python def parse_pytest_results(results: Dict) -> Dict[str, Any]: """Parse pytest JSON report format with custom metrics.""" summary = results.get("summary", {}) # Add custom metric: coverage percentage coverage = results.get("coverage", {}).get("percent_covered", 0) return { "platform": "backend", "total": summary.get("total", 0), "passed": summary.get("passed", 0), "failed": summary.get("failed", 0), "skipped": summary.get("skipped", 0), "duration": summary.get("duration", 0), "pass_rate": round(pass_rate, 2), "coverage": coverage, # Custom metric } ``` **Step 2: Update aggregation function:** ```python def aggregate_platform_status(platforms: List[Dict[str, Any]]) -> Dict[str, Any]: """Calculate aggregate metrics with custom fields.""" total_tests = sum(p.get("total", 0) for p in platforms) total_passed = sum(p.get("passed", 0) for p in platforms) # Add custom metric: average coverage coverages = [p.get("coverage", 0) for p in platforms if "coverage" in p] avg_coverage = sum(coverages) / len(coverages) if coverages else 0 return { "total_tests": total_tests, "total_passed": total_passed, "total_failed": total_failed, "pass_rate": round(pass_rate, 2), "total_duration_seconds": total_duration, "platform_count": len(platforms), "avg_coverage": round(avg_coverage, 2), # Custom metric } ``` **Step 3: Update markdown summary generator:** ```python def generate_markdown_summary( aggregate: Dict[str, Any], platforms: List[Dict[str, Any]], ) -> str: """Generate markdown summary with custom metrics.""" lines = [ "# CI Test Results Summary", f"Generated: {datetime.now().isoformat()}", "", "## Overall Results", f"- **Total Tests**: {aggregate['total_tests']}", f"- **Passed**: {aggregate['total_passed']}", f"- **Failed**: {aggregate['total_failed']}", f"- **Pass Rate**: {aggregate['pass_rate']}%", f"- **Avg Coverage**: {aggregate['avg_coverage']}%", # Custom metric f"- **Duration**: {aggregate['total_duration_seconds']}s", "", "## Platform Breakdown", "| Platform | Tests | Passed | Failed | Pass Rate | Coverage | Duration |", # Custom column "|----------|-------|--------|--------|-----------|----------|----------|", ] for p in platforms: platform = p["platform"].upper() lines.append( f"| {platform} | {p['total']} | {p['passed']} | {p['failed']} | " f"{p['pass_rate']:.1f}% | {p.get('coverage', 0):.1f}% | {p['duration']}s |" # Custom column ) return "\n".join(lines) ``` ### Creating Custom Status Checks **GitHub Actions Status Check API:** ```yaml # In unified-tests-parallel.yml aggregation job - name: Create per-platform status checks uses: actions/github-script@v7 with: script: | const fs = require('fs'); const unified = JSON.parse(fs.readFileSync('results/ci_status.json', 'utf8')); // Create status check for each platform for (const platform of unified.platforms) { const context = `ci/${platform.platform}-tests`; const state = platform.failed === 0 ? 'success' : 'failure'; const description = `${platform.passed}/${platform.total} passed (${platform.pass_rate}%)`; await github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.sha, context: context, state: state, description: description, target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, }); } ``` ### Integrating with External Dashboards **Grafana Integration:** ```python # Add to ci_status_aggregator.py import requests def send_to_grafana(aggregate: Dict[str, Any]): """Send metrics to Grafana via Loki or Prometheus.""" grafana_url = os.getenv("GRAFANA_URL") if not grafana_url: return metrics = { "pass_rate": aggregate["pass_rate"], "total_tests": aggregate["total_tests"], "total_failed": aggregate["total_failed"], "duration": aggregate["total_duration_seconds"], } requests.post(grafana_url, json=metrics) ``` **Datadog Integration:** ```python # Add to ci_status_aggregator.py from datadog import DogStatsd def send_to_datadog(aggregate: Dict[str, Any]): """Send metrics to Datadog.""" statsd = DogStatsd() statsd.gauge('ci.pass_rate', aggregate["pass_rate"]) statsd.gauge('ci.total_tests', aggregate["total_tests"]) statsd.gauge('ci.total_failed', aggregate["total_failed"]) statsd.gauge('ci.duration', aggregate["total_duration_seconds"]) ``` ### Trending Data Storage **Historical Data Location:** `backend/tests/coverage_reports/metrics/ci_status.json` **Trend File Structure:** ```json [ { "timestamp": "2026-03-07T15:30:00.000000Z", "aggregate": { "total_tests": 2181, "total_passed": 2150, "total_failed": 31, "pass_rate": 98.58 }, "platforms": [ { "platform": "backend", "total": 500, "passed": 480, "failed": 20, "duration": 580 } ] }, { "timestamp": "2026-03-07T16:00:00.000000Z", "aggregate": { "total_tests": 2185, "total_passed": 2160, "total_failed": 25, "pass_rate": 98.86 }, "platforms": [...] } ] ``` **Loading Historical Data:** ```python import json from pathlib import Path def load_trend_history(trend_file: str) -> List[Dict[str, Any]]: """Load historical trend data from JSON.""" path = Path(trend_file) if not path.exists(): return [] try: history = json.loads(path.read_text()) history.sort(key=lambda x: x.get("timestamp", ""), reverse=True) return history except (json.JSONDecodeError, KeyError): return [] ``` --- ## Platform Retry Flow ### When Retries Trigger **Trigger Condition:** - `unified-tests-parallel` workflow completes with `conclusion: 'failure'` - `platform-retry.yml` workflow_run trigger activates automatically - Only runs on `main` and `develop` branches **Retry Workflow:** 1. **detect-failures job** downloads artifacts from failed workflow run 2. **platform_retry_router.py** extracts failed tests for each platform 3. **Conditional retry jobs** run only for platforms with failures 4. **Retry results** uploaded as artifacts (30-day retention) ### How platform_retry_router.py Extracts Failed Tests **Backend (pytest format):** ```python def extract_failed_tests(results: Dict, platform: str) -> List[str]: """Extract failed test names from pytest JSON report.""" if platform == "backend": # pytest format: summary.failed + test results failed_tests = [] for test in results.get("tests", []): if test.get("outcome") == "failed": failed_tests.append(test["name"]) return failed_tests ``` **Frontend/Mobile (Jest format):** ```python def extract_failed_tests(results: Dict, platform: str) -> List[str]: """Extract failed test names from Jest JSON results.""" if platform in ["frontend", "mobile"]: # Jest format: testResults with status failed_tests = [] for suite in results.get("testResults", []): for test in suite.get("assertionResults", []): if test.get("status") == "failed": failed_tests.append(test["fullName"]) return failed_tests ``` **Desktop (cargo test format):** ```python def extract_failed_tests(results: Dict, platform: str) -> List[str]: """Extract failed test names from cargo test JSON.""" if platform == "desktop": # Cargo format: testResults with passed field failed_tests = [] for test in results.get("testResults", []): if not test.get("passed", False): failed_tests.append(test["name"]) return failed_tests ``` ### Retry Job Execution Flow **1. detect-failures job:** ```yaml - name: Check backend failures id: check-backend run: | python backend/tests/scripts/platform_retry_router.py \ --platform backend \ --results-file results/backend/pytest_report.json \ --output-file retry_commands/backend_retry.sh if [ $? -eq 0 ]; then echo "failed=true" >> $GITHUB_OUTPUT else echo "failed=false" >> $GITHUB_OUTPUT fi ``` **2. Conditional retry job:** ```yaml retry-backend: name: Retry Backend Tests needs: [detect-failures] if: ${{ needs.detect-failures.outputs.backend-failed == 'true' }} steps: - name: Run backend retry tests run: | bash ../retry_commands/backend_retry.sh ``` **3. Retry result aggregation:** ```yaml - name: Upload retry results uses: actions/upload-artifact@v4 with: name: backend-retry-results path: backend/pytest_report.json retention-days: 7 ``` ### Retry Result Aggregation **Actual platform_retry_router.py Output:** ```json { "platform": "backend", "failed_tests": [ "tests/test_agent_governance.py::test_agent_permission_check", "tests/test_llm_byok.py::test_streaming_response" ], "retry_command": "pytest tests/ -v -n auto tests/test_agent_governance.py::test_agent_permission_check tests/test_llm_byok.py::test_streaming_response --json-report --json-report-file=pytest_report.json", "exit_code": 0 } ``` **platform-retry.yml detect-failures Job Output:** ```yaml outputs: backend-failed: 'true' # or 'false' frontend-failed: 'false' mobile-failed: 'false' desktop-failed: 'true' ``` **Conditional Retry Job Execution:** ```yaml retry-backend: name: Retry Backend Tests needs: [detect-failures] if: ${{ needs.detect-failures.outputs.backend-failed == 'true' }} # Only runs if backend-failed == 'true' ``` **Retry Results Upload:** ```yaml - name: Upload retry results uses: actions/upload-artifact@v4 if: always() with: name: backend-retry-results path: backend/pytest_report.json retention-days: 7 if-no-files-found: warn ``` **Current State:** Retry results uploaded as artifacts, but not aggregated into unified status **Future Enhancement:** Add retry aggregation to ci_status_aggregator.py ```python def aggregate_retry_results( original_results: Dict[str, Any], retry_results: Dict[str, Any], ) -> Dict[str, Any]: """Aggregate original and retry results.""" # Combine original + retry metrics # Update platform status with retry pass rate # Generate summary with retry information pass ``` --- ## Best Practices ### Development Workflow **1. Run tests locally before pushing:** ```bash # Backend cd backend && pytest tests/ -v -n auto # Frontend cd frontend-nextjs && npm run test:ci -- --maxWorkers=2 # Mobile cd mobile && npm run test:ci -- --maxWorkers=2 # Desktop cd frontend-nextjs/src-tauri && cargo test --test-threads=4 ``` **2. Use PR drafts for experimental changes:** - Create draft PR to trigger CI without blocking others - Review CI results before marking PR as ready for review - Fix failing tests before requesting review **3. Monitor CI duration trends:** - Check ci_status.json for pass rate trends - Identify platforms with declining pass rates - Optimize slow tests (target: <15 minutes total) ### CI/CD Maintenance **1. Update baseline timings quarterly:** - Measure actual execution time for each platform - Update PARALLEL_EXECUTION_GUIDE.md timing benchmarks table - Adjust timeout values if needed **2. Review cache hit rates monthly:** - Check job logs for cache hit/miss messages - Optimize cache keys if hit rate <80% - Clean up old cache entries if approaching 10 GB limit **3. Audit test suite growth:** - Track new test additions in ci_status.json - Remove obsolete or duplicate tests - Ensure test count growth aligns with feature development ### Performance Optimization **1. Optimize slowest platform first:** - Identify slowest platform in CI dashboard - Focus optimization efforts on bottleneck (max(platform_durations)) - Target: All platforms <15 minutes **2. Use test splitting strategically:** - Only split if platform duration >15 minutes - Start with pytest-xdist auto balancing (simpler) - Move to manual sharding only if auto balancing insufficient **3. Leverage caching aggressively:** - Cache pip packages, npm modules, cargo registry - Use restore-keys for fallback cache keys - Verify cache hit rate in job logs --- ## Glossary - **Matrix Strategy:** GitHub Actions feature for running multiple jobs in parallel with different configurations - **Fail-Fast:** Workflow setting that cancels all jobs if one job fails (disabled in our workflow) - **Max-Parallel:** Limit on concurrent matrix jobs (set to 4 for our 4 platforms) - **Aggregation Job:** Job that combines results from multiple platform jobs into unified report - **Retry Workflow:** Separate workflow that re-runs only failed platform tests (not full suite) - **pytest-xdist:** pytest plugin for parallel test execution with automatic load balancing - **Jest Sharding:** Built-in Jest feature for splitting test suite across multiple jobs - **cargo test --test-threads:** Rust cargo test option for parallel thread execution - **Cache Hit Rate:** Percentage of time CI cache is successfully restored (target: >80%) - **Flaky Test:** Test that passes/fails intermittently without code changes - **Platform-Specific Retry:** Re-run only failed tests for a specific platform (80% time savings vs full suite) --- ## Additional Resources **Internal Documentation:** - `.planning/phases/149-quality-infrastructure-parallel/149-RESEARCH.md` - Research document - `backend/tests/docs/E2E_TESTING_GUIDE.md` - E2E testing patterns - `backend/tests/docs/COVERAGE_GUIDE.md` - Coverage reporting guide - `backend/tests/docs/FLAKY_TEST_GUIDE.md` - Flaky test handling guide **External References:** - [GitHub Actions Matrix Strategy](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs) - [pytest-xdist Documentation](https://pytest-xdist.readthedocs.io/) - [Jest CLI Options](https://jestjs.io/docs/cli) - [cargo test Documentation](https://doc.rust-lang.org/cargo/commands/cargo-test.html) **Related Workflows:** - `.github/workflows/unified-tests.yml` - Sequential test execution (backup) - `.github/workflows/e2e-unified.yml` - E2E test orchestration (Phase 148) - `.github/workflows/platform-retry.yml` - Platform-specific retry jobs --- **Document Version:** 1.0 **Last Updated:** March 7, 2026 **Maintainer:** Atom CI/CD Team **Feedback:** Open issue or PR for improvements to this guide ## See Also - [Testing Documentation Index](../../docs/TESTING_INDEX.md) - Central hub for all testing documentation - [Testing Onboarding Guide](../../docs/TESTING_ONBOARDING.md) - 15-minute quick start for all platforms - [Flaky Test Quarantine](FLAKY_TEST_QUARANTINE.md) - Multi-run detection in CI/CD - [Coverage Trending Guide](COVERAGE_TRENDING_GUIDE.md) - Track coverage over time - [Test Isolation Patterns](TEST_ISOLATION_PATTERNS.md) - Writing tests for parallel execution