diff --git a/.changeset/major-sites-matter.md b/.changeset/major-sites-matter.md new file mode 100644 index 00000000..22c4c71c --- /dev/null +++ b/.changeset/major-sites-matter.md @@ -0,0 +1,5 @@ +--- +'@mcpmux/desktop': patch +--- + +Release test diff --git a/.config/nextest.toml b/.config/nextest.toml index ed005227..1af20aa3 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -16,3 +16,24 @@ final-status-level = "slow" [profile.ci.junit] path = "junit.xml" report-name = "mcpmux-tests" + +# Separate profiles for granular test reporting +[profile.ci-unit] +fail-fast = false +retries = 3 +status-level = "retry" +final-status-level = "slow" + +[profile.ci-unit.junit] +path = "junit-unit.xml" +report-name = "mcpmux-unit-tests" + +[profile.ci-integration] +fail-fast = false +retries = 3 +status-level = "retry" +final-status-level = "slow" + +[profile.ci-integration.junit] +path = "junit-integration.xml" +report-name = "mcpmux-integration-tests" diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index 1586248f..f4e494e8 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -1,5 +1,5 @@ name: Install Linux deps -description: Install build and runtime deps for Tauri app on Ubuntu +description: Install build and runtime deps for Tauri app on Ubuntu (with caching) inputs: e2e: description: Include E2E desktop deps (webkit2gtk-driver, xvfb, gnome-keyring) @@ -12,22 +12,39 @@ inputs: runs: using: composite steps: - - name: Install Linux deps + # Cache apt packages to avoid re-downloading on every run + - name: Cache apt packages (base) + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: build-essential pkg-config libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsecret-1-dev + version: 1.0 + + - name: Cache apt packages (E2E) + if: ${{ inputs.e2e == 'true' }} + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: webkit2gtk-driver xvfb gnome-keyring gsettings-desktop-schemas dbus-x11 at-spi2-core libglib2.0-bin + version: 1.2 + + # Compile gsettings schemas (required after restore from cache) + - name: Compile gsettings schemas + if: ${{ inputs.e2e == 'true' }} shell: bash run: | - BASE_DEPS="build-essential pkg-config libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsecret-1-dev" - E2E_DEPS="webkit2gtk-driver xvfb gnome-keyring" - sudo apt-get update - if [ "${{ inputs.e2e }}" = "true" ]; then - sudo apt-get install -y $BASE_DEPS $E2E_DEPS - else - sudo apt-get install -y $BASE_DEPS - fi - if [ "${{ inputs.verify_glib }}" = "true" ]; then - echo "=== Verifying glib-2.0 installation ===" - dpkg -l | grep libglib2.0-dev || echo "libglib2.0-dev not installed" - find /usr -name 'glib-2.0.pc' 2>/dev/null || echo "glib-2.0.pc not found" - pkg-config --modversion glib-2.0 || echo "pkg-config cannot find glib-2.0" - pkg-config --exists 'glib-2.0 >= 2.70' && echo "glib-2.0 >= 2.70 found" || (echo "ERROR: glib-2.0 >= 2.70 not found"; exit 1) - fi - echo "PKG_CONFIG_PATH=$(pkg-config --variable pc_path pkg-config)" >> $GITHUB_ENV + sudo glib-compile-schemas /usr/share/glib-2.0/schemas/ || true + # Verify the schema is available + gsettings list-schemas | grep -q org.gnome.system.proxy && echo "✓ org.gnome.system.proxy schema available" || echo "⚠ schema not found" + + - name: Verify glib installation + if: ${{ inputs.verify_glib == 'true' }} + shell: bash + run: | + echo "=== Verifying glib-2.0 installation ===" + dpkg -l | grep libglib2.0-dev || echo "libglib2.0-dev not installed" + find /usr -name 'glib-2.0.pc' 2>/dev/null || echo "glib-2.0.pc not found" + pkg-config --modversion glib-2.0 || echo "pkg-config cannot find glib-2.0" + pkg-config --exists 'glib-2.0 >= 2.70' && echo "glib-2.0 >= 2.70 found" || (echo "ERROR: glib-2.0 >= 2.70 not found"; exit 1) + + - name: Set PKG_CONFIG_PATH + shell: bash + run: echo "PKG_CONFIG_PATH=$(pkg-config --variable pc_path pkg-config)" >> $GITHUB_ENV diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af69ef4d..259cad93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,18 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} +# Permissions needed for test reporting and PR comments +permissions: + contents: read + actions: read + checks: write + pull-requests: write + env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 + # Faster git fetches for cargo + CARGO_NET_GIT_FETCH_WITH_CLI: true # Cache keys for better hit rates RUST_CACHE_KEY: v1 PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright @@ -74,11 +83,41 @@ jobs: node-version: 20 cache: 'pnpm' + # Cache pnpm store for faster installs + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + pnpm-store-${{ runner.os }}- + - run: pnpm install --frozen-lockfile - run: pnpm typecheck - run: pnpm lint - - name: TypeScript tests - run: pnpm test:ts + + - name: TypeScript tests with coverage + run: pnpm exec vitest run -c tests/ts/vitest.config.ts --coverage + + - name: Upload TS test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-typescript + path: tests/ts/test-results/ + retention-days: 7 + + - name: Upload TS coverage + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-typescript + path: tests/ts/coverage/ + retention-days: 7 # ───────────────────────────────────────────────────────────── # Rust Tests (cross-platform matrix) @@ -108,9 +147,12 @@ jobs: with: targets: ${{ matrix.target }} + # Share cache between rust-test and build jobs for faster compilation - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.target }} + shared-key: rust-${{ matrix.target }} + cache-on-failure: true # Install cargo-nextest - name: Install nextest @@ -118,63 +160,200 @@ jobs: # Run unit tests (fast, no external deps) - name: Unit tests - run: cargo nextest run --workspace --lib --profile ci + run: cargo nextest run --workspace --lib --profile ci-unit # Run doc tests (nextest doesn't support) - name: Doc tests run: cargo test --workspace --doc - # Run integration tests + # Run integration tests (outputs JUnit XML) - name: Integration tests - run: cargo nextest run -p tests --profile ci + run: cargo nextest run -p tests --profile ci-integration + + # Upload test results for reporting (separate artifacts per type) + - name: Upload unit test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-rust-unit-${{ matrix.os }} + path: target/nextest/ci-unit/junit-unit.xml + retention-days: 7 + + - name: Upload integration test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-rust-integration-${{ matrix.os }} + path: target/nextest/ci-integration/junit-integration.xml + retention-days: 7 # ───────────────────────────────────────────────────────────── - # Build Verification (ensures app compiles on all platforms) + # Build Verification (macOS only - Linux/Windows covered by e2e-desktop) # ───────────────────────────────────────────────────────────── build: needs: [rust-check, ts-check] - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - os: windows-latest - target: x86_64-pc-windows-msvc - - os: macos-latest - target: aarch64-apple-darwin - - runs-on: ${{ matrix.os }} + runs-on: macos-latest + env: + MACOSX_DEPLOYMENT_TARGET: '10.13' steps: - uses: actions/checkout@v4 - - name: Install Linux deps - if: matrix.os == 'ubuntu-latest' - uses: ./.github/actions/install-linux-deps - - uses: dtolnay/rust-toolchain@stable with: - targets: ${{ matrix.target }} - env: - PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig + targets: aarch64-apple-darwin - uses: Swatinem/rust-cache@v2 with: - key: ${{ matrix.target }}-build - env: - PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig + key: aarch64-apple-darwin + shared-key: rust-aarch64-apple-darwin + cache-on-failure: true + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' + # Cache pnpm store + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + pnpm-store-${{ runner.os }}- + + # Cache Tauri CLI binary + - name: Cache Tauri CLI + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/cargo-tauri* + ~/.cargo/bin/tauri* + key: tauri-cli-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + tauri-cli-${{ runner.os }}- + - run: pnpm install --frozen-lockfile - run: pnpm build env: - PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + # ───────────────────────────────────────────────────────────── + # Test Results Report (separate checks per test type and OS) + # Uses dorny/test-reporter for granular GitHub Check Runs + # ───────────────────────────────────────────────────────────── + test-report: + needs: [rust-test, ts-check] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download all test results + uses: actions/download-artifact@v4 + with: + pattern: test-results-* + path: test-results + + # Rust Unit Tests - Per OS + - name: 'Report: Rust Unit Tests (Linux)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🦀 Rust Unit Tests (Linux)' + path: test-results/test-results-rust-unit-ubuntu-latest/*.xml + reporter: java-junit + fail-on-error: false + + - name: 'Report: Rust Unit Tests (Windows)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🦀 Rust Unit Tests (Windows)' + path: test-results/test-results-rust-unit-windows-latest/*.xml + reporter: java-junit + fail-on-error: false + + - name: 'Report: Rust Unit Tests (macOS)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🦀 Rust Unit Tests (macOS)' + path: test-results/test-results-rust-unit-macos-latest/*.xml + reporter: java-junit + fail-on-error: false + + # Rust Integration Tests - Per OS + - name: 'Report: Rust Integration Tests (Linux)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🔗 Rust Integration Tests (Linux)' + path: test-results/test-results-rust-integration-ubuntu-latest/*.xml + reporter: java-junit + fail-on-error: false + + - name: 'Report: Rust Integration Tests (Windows)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🔗 Rust Integration Tests (Windows)' + path: test-results/test-results-rust-integration-windows-latest/*.xml + reporter: java-junit + fail-on-error: false + + - name: 'Report: Rust Integration Tests (macOS)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🔗 Rust Integration Tests (macOS)' + path: test-results/test-results-rust-integration-macos-latest/*.xml + reporter: java-junit + fail-on-error: false + + # TypeScript Tests + - name: 'Report: TypeScript Tests' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '📘 TypeScript Tests' + path: test-results/test-results-typescript/*.xml + reporter: jest-junit + fail-on-error: false + + # ───────────────────────────────────────────────────────────── + # Coverage Report (uploads to Codecov) + # ───────────────────────────────────────────────────────────── + coverage-report: + needs: [ts-check] + if: always() && needs.ts-check.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + with: + pattern: coverage-* + path: coverage + merge-multiple: true + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + files: coverage/lcov.info + flags: typescript + name: mcpmux-ts-coverage + fail_ci_if_error: false + verbose: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # ───────────────────────────────────────────────────────────── # E2E Tests (Web-only Playwright for UI smoke tests) # Skip with [skip e2e] in commit message for faster PR iteration @@ -208,40 +387,30 @@ jobs: - name: Run web-only E2E tests run: pnpm test:e2e:web --project=chromium - - name: Upload test results + - name: Upload E2E web test results uses: actions/upload-artifact@v4 - if: failure() + if: always() with: - name: playwright-report + name: test-results-e2e-web path: tests/e2e/reports/ retention-days: 7 - # ───────────────────────────────────────────────────────────── - # E2E Desktop trigger check: main, [e2e] in commit, or /e2e-desktop comment on PR - # ───────────────────────────────────────────────────────────── - e2e-trigger-check: - runs-on: ubuntu-latest - permissions: - pull-requests: read - contents: read - outputs: - run_e2e: ${{ steps.check.outputs.should_run }} - steps: - - name: Check if e2e-desktop should run - id: check - uses: ./.github/actions/check-comment-trigger + - name: 'Report: E2E Web Tests' + uses: dorny/test-reporter@v2 + if: always() with: - trigger: '/e2e-desktop' - check_main: 'true' - check_commit: 'true' + name: '🌐 E2E Web Tests' + path: tests/e2e/reports/junit.xml + reporter: java-junit + fail-on-error: false # ───────────────────────────────────────────────────────────── - # E2E Tests (Desktop app with WebDriver - Linux/Windows only) - # Runs when: main branch, [e2e] in commit, or /e2e-desktop comment on PR + # E2E Tests (Desktop app with WebDriver - Linux/Windows) + # Runs on all commits; skip with [skip e2e] in commit message # ───────────────────────────────────────────────────────────── e2e-desktop: - needs: [build, e2e-trigger-check] - if: needs.e2e-trigger-check.outputs.run_e2e == 'true' + needs: [rust-check, ts-check] + if: "!contains(github.event.head_commit.message, '[skip e2e]')" uses: ./.github/workflows/e2e-desktop.yml with: ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/e2e-desktop-comment.yml b/.github/workflows/e2e-desktop-comment.yml deleted file mode 100644 index 3c3100de..00000000 --- a/.github/workflows/e2e-desktop-comment.yml +++ /dev/null @@ -1,58 +0,0 @@ -# Trigger desktop E2E tests when someone comments /e2e-desktop on a PR, or manually. -# Calls the reusable e2e-desktop workflow with the PR head ref. - -name: E2E Desktop (Comment Trigger) - -on: - issue_comment: - types: [created] - workflow_dispatch: - inputs: - pr: - description: PR number to run E2E on (required for manual run) - required: true - type: number - -jobs: - check-trigger: - runs-on: ubuntu-latest - outputs: - should_run: ${{ steps.check.outputs.should_run }} - steps: - - name: Check comment trigger - id: check - uses: ./.github/actions/check-comment-trigger - with: - trigger: '/e2e-desktop' - allow_workflow_dispatch: 'true' - - get-pr-ref: - needs: check-trigger - if: needs.check-trigger.outputs.should_run == 'true' - runs-on: ubuntu-latest - permissions: - pull-requests: read - outputs: - ref: ${{ steps.pr.outputs.sha }} - steps: - - name: Get PR head ref - id: pr - uses: actions/github-script@v7 - with: - script: | - const prNumber = context.eventName === 'workflow_dispatch' - ? context.payload.inputs.pr - : context.issue.number; - const pr = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: Number(prNumber), - }); - core.setOutput('sha', pr.data.head.sha); - - e2e-desktop: - needs: get-pr-ref - uses: ./.github/workflows/e2e-desktop.yml - with: - ref: ${{ needs.get-pr-ref.outputs.ref }} - secrets: inherit diff --git a/.github/workflows/e2e-desktop.yml b/.github/workflows/e2e-desktop.yml index bc51d1b5..74f48525 100644 --- a/.github/workflows/e2e-desktop.yml +++ b/.github/workflows/e2e-desktop.yml @@ -44,6 +44,8 @@ jobs: - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.os }}-e2e-desktop + shared-key: rust-${{ matrix.os == 'ubuntu-latest' && 'x86_64-unknown-linux-gnu' || 'x86_64-pc-windows-msvc' }} + cache-on-failure: true env: PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig @@ -53,6 +55,20 @@ jobs: node-version: 20 cache: 'pnpm' + # Cache pnpm store for faster installs + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + pnpm-store-${{ runner.os }}- + - name: Cache tauri-driver uses: actions/cache@v4 id: tauri-driver-cache @@ -66,6 +82,17 @@ jobs: env: PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig + # Cache Tauri CLI binary (avoid recompiling on every run) + - name: Cache Tauri CLI + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/cargo-tauri* + ~/.cargo/bin/tauri* + key: tauri-cli-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + tauri-cli-${{ runner.os }}- + - run: pnpm install --frozen-lockfile - name: Build app @@ -77,8 +104,63 @@ jobs: - name: Run desktop E2E tests (Linux) if: matrix.os == 'ubuntu-latest' run: | - dbus-run-session -- bash -c 'gnome-keyring-daemon --unlock < /dev/null; sleep 2; xvfb-run --auto-servernum pnpm test:e2e' + # Start dbus session and unlock gnome-keyring with a dummy password for CI + dbus-run-session -- bash -c ' + echo "test" | gnome-keyring-daemon --unlock --components=secrets + export $(gnome-keyring-daemon --start --components=secrets) + sleep 1 + xvfb-run --auto-servernum pnpm test:e2e + ' - name: Run desktop E2E tests (Windows) if: matrix.os == 'windows-latest' run: pnpm test:e2e + + # Upload test results and artifacts + - name: Upload E2E test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-desktop-results-${{ matrix.os }} + path: | + tests/e2e/reports/ + tests/e2e/screenshots/ + tests/e2e/videos/ + retention-days: 7 + + # Publish E2E Desktop test results with separate checks per OS + e2e-report: + needs: [e2e-desktop] + if: always() + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + checks: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Download all E2E test results + uses: actions/download-artifact@v4 + with: + pattern: e2e-desktop-results-* + path: test-results + + - name: 'Report: E2E Desktop Tests (Linux)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🖥️ E2E Desktop Tests (Linux)' + path: test-results/e2e-desktop-results-ubuntu-latest/**/*.xml + reporter: java-junit + fail-on-error: false + + - name: 'Report: E2E Desktop Tests (Windows)' + uses: dorny/test-reporter@v2 + if: always() + with: + name: '🖥️ E2E Desktop Tests (Windows)' + path: test-results/e2e-desktop-results-windows-latest/**/*.xml + reporter: java-junit + fail-on-error: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63019183..fcb65b14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thank you for your interest in contributing! ## License -This project is licensed under the [Elastic License 2.0 (ELv2)](LICENSE). By contributing, you agree that your contributions will be licensed under the same license. +This project is licensed under the [GNU General Public License v3.0 (GPL-3.0)](LICENSE). By contributing, you agree that your contributions will be licensed under the same license. ## Developer Certificate of Origin (DCO) diff --git a/apps/desktop/src/features/featuresets/FeatureSetPanel.tsx b/apps/desktop/src/features/featuresets/FeatureSetPanel.tsx index b3c6cd03..3e603f98 100644 --- a/apps/desktop/src/features/featuresets/FeatureSetPanel.tsx +++ b/apps/desktop/src/features/featuresets/FeatureSetPanel.tsx @@ -327,6 +327,7 @@ export function FeatureSetPanel({ featureSet, spaceId, onClose, onDelete, onUpda