name: Test Performance Agent

on:
  workflow_run: # zizmor: ignore[dangerous-triggers] main-only test optimization after trusted CI; job gates repository, event, branch, actor, conclusion, current main SHA, and daily cadence before using write token
    workflows:
      - CI
    types:
      - completed
  workflow_dispatch:

permissions:
  actions: read
  contents: write

concurrency:
  group: test-performance-agent-main
  cancel-in-progress: false

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
  TEST_PERF_BEFORE: .artifacts/test-perf/baseline-before.json
  TEST_PERF_AFTER: .artifacts/test-perf/after-agent.json
  TEST_PERF_COMPARE: .artifacts/test-perf/agent-compare.json

jobs:
  optimize-tests:
    if: >
      github.repository == 'openclaw/openclaw' &&
      (github.event_name == 'workflow_dispatch' ||
        (github.event.workflow_run.conclusion == 'success' &&
          github.event.workflow_run.event == 'push' &&
          github.event.workflow_run.head_branch == 'main' &&
          !endsWith(github.event.workflow_run.actor.login, '[bot]')))
    runs-on: ubuntu-24.04
    timeout-minutes: 240
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: main
          fetch-depth: 0
          persist-credentials: false
          submodules: false

      - name: Gate trusted main activity and daily cadence
        id: gate
        env:
          EVENT_NAME: ${{ github.event_name }}
          GH_TOKEN: ${{ github.token }}
          WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
        run: |
          set -euo pipefail

          if [ "$EVENT_NAME" != "workflow_run" ]; then
            echo "run_agent=true" >> "$GITHUB_OUTPUT"
            echo "base_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          for attempt in 1 2 3 4 5; do
            if git fetch --no-tags origin main; then
              break
            fi
            if [ "$attempt" = "5" ]; then
              echo "Failed to fetch main after retries." >&2
              exit 1
            fi
            echo "Fetch attempt ${attempt} failed; retrying."
            sleep $((attempt * 2))
          done

          remote_main="$(git rev-parse origin/main)"
          if [ "$remote_main" != "$WORKFLOW_HEAD_SHA" ]; then
            echo "CI run is superseded by ${remote_main}; skipping test performance agent for ${WORKFLOW_HEAD_SHA}."
            echo "run_agent=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          day_start="$(date -u +%Y-%m-%dT00:00:00Z)"
          runs_json="$RUNNER_TEMP/test-performance-agent-runs.json"
          gh api --method GET "repos/${GITHUB_REPOSITORY}/actions/workflows/test-performance-agent.yml/runs" \
            -f branch=main \
            -f event=workflow_run \
            -f per_page=50 > "$runs_json"

          prior_runs="$(
            jq -r \
              --argjson current_run_id "$GITHUB_RUN_ID" \
              --arg day_start "$day_start" \
              '.workflow_runs[]
                | select(.database_id != $current_run_id)
                | select(.created_at >= $day_start)
                | select(.status != "cancelled")
                | select((.conclusion // "") != "skipped")
                | [.database_id, .status, (.conclusion // ""), .created_at, .head_sha]
                | @tsv' "$runs_json"
          )"

          if [ -n "$prior_runs" ]; then
            echo "Test performance agent already ran or is running today; skipping."
            printf '%s\n' "$prior_runs"
            echo "run_agent=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "run_agent=true" >> "$GITHUB_OUTPUT"
          echo "base_sha=${remote_main}" >> "$GITHUB_OUTPUT"

      - name: Setup Node environment
        if: steps.gate.outputs.run_agent == 'true'
        uses: ./.github/actions/setup-node-env
        with:
          install-bun: "false"

      - name: Ensure test performance agent key exists
        if: steps.gate.outputs.run_agent == 'true'
        env:
          OPENAI_API_KEY: ${{ secrets.OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
        run: |
          set -euo pipefail
          if [ -z "${OPENAI_API_KEY:-}" ]; then
            echo "Missing OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2
            exit 1
          fi

      - name: Build baseline full-suite performance report
        if: steps.gate.outputs.run_agent == 'true'
        run: pnpm test:perf:groups --full-suite --allow-failures --output "$TEST_PERF_BEFORE" --limit 20 --top-files 40

      - name: Run Codex test performance agent
        if: steps.gate.outputs.run_agent == 'true'
        uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
        with:
          openai-api-key: ${{ secrets.OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
          prompt-file: .github/codex/prompts/test-performance-agent.md
          model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
          effort: high
          sandbox: workspace-write
          safety-strategy: drop-sudo
          codex-args: '["--full-auto"]'

      - name: Enforce focused test performance patch
        if: steps.gate.outputs.run_agent == 'true'
        id: patch
        run: |
          set -euo pipefail

          untracked="$(git ls-files --others --exclude-standard)"
          if [ -n "$untracked" ]; then
            echo "Test performance agent created untracked files; forbidden:"
            printf '%s\n' "$untracked"
            exit 1
          fi

          added_deleted_or_renamed="$(git diff --name-status --diff-filter=ADR)"
          if [ -n "$added_deleted_or_renamed" ]; then
            echo "Test performance agent added, deleted, or renamed tracked files; forbidden:"
            printf '%s\n' "$added_deleted_or_renamed"
            exit 1
          fi

          bad_paths="$(
            git diff --name-only | while IFS= read -r path; do
              case "$path" in
                apps/*|extensions/*|packages/*|scripts/*|src/*|test/*|ui/*) ;;
                *) printf '%s\n' "$path" ;;
              esac
            done
          )"
          if [ -n "$bad_paths" ]; then
            echo "Test performance agent touched forbidden paths:"
            printf '%s\n' "$bad_paths"
            exit 1
          fi

          if git diff --quiet; then
            echo "has_changes=false" >> "$GITHUB_OUTPUT"
          else
            echo "has_changes=true" >> "$GITHUB_OUTPUT"
          fi

      - name: Restore Node 24 path
        if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
        run:
          | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
          set -euo pipefail
          export PATH="${NODE_BIN}:${PATH}"
          echo "${NODE_BIN}" >> "$GITHUB_PATH"
          node -v
          corepack enable
          pnpm -v

      - name: Run full-suite performance report after agent changes
        if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
        run: pnpm test:perf:groups --full-suite --output "$TEST_PERF_AFTER" --limit 20 --top-files 40

      - name: Compare test performance reports
        if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
        run: pnpm test:perf:groups:compare "$TEST_PERF_BEFORE" "$TEST_PERF_AFTER" --output "$TEST_PERF_COMPARE" --limit 20 --top-files 40

      - name: Enforce coverage-preserving test count
        if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
        run: |
          set -euo pipefail
          node <<'NODE'
          const fs = require("node:fs");
          const before = JSON.parse(fs.readFileSync(process.env.TEST_PERF_BEFORE, "utf8"));
          const after = JSON.parse(fs.readFileSync(process.env.TEST_PERF_AFTER, "utf8"));

          if (before.failed) {
            console.log("Baseline had failing configs; skipping total test-count comparison against partial report.");
            process.exit(0);
          }

          const beforeTests = before.totals?.testCount ?? 0;
          const afterTests = after.totals?.testCount ?? 0;
          if (afterTests < beforeTests) {
            console.error(`Test count decreased from ${beforeTests} to ${afterTests}; refusing coverage-reducing patch.`);
            process.exit(1);
          }
          console.log(`Test count preserved: ${beforeTests} -> ${afterTests}.`);
          NODE

      - name: Check changed lanes
        if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
        run: pnpm check:changed

      - name: Commit test performance updates
        if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
        env:
          GITHUB_TOKEN: ${{ github.token }}
          TARGET_BRANCH: main
        run: |
          set -euo pipefail

          if git diff --quiet; then
            echo "No test performance changes."
            exit 0
          fi

          git config user.name "openclaw-test-performance-agent[bot]"
          git config user.email "openclaw-test-performance-agent[bot]@users.noreply.github.com"
          git add apps extensions packages scripts src test ui
          git commit --no-verify -m "test: optimize slow tests"

          for attempt in 1 2 3 4 5; do
            if ! git fetch --no-tags origin "${TARGET_BRANCH}"; then
              echo "Fetch attempt ${attempt} failed; retrying."
              sleep $((attempt * 2))
              continue
            fi
            if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:"${TARGET_BRANCH}"; then
              exit 0
            fi
            remote_main="$(git rev-parse "origin/${TARGET_BRANCH}")"
            if [ "$remote_main" != "$(git rev-parse HEAD^)" ]; then
              echo "main advanced; rebasing test performance update onto ${remote_main}."
              if ! git rebase "origin/${TARGET_BRANCH}"; then
                echo "Test performance update no longer applies cleanly; skipping stale update."
                git rebase --abort || true
                exit 0
              fi
              pnpm check:changed
            fi
            echo "Test performance update attempt ${attempt} failed; retrying."
            sleep $((attempt * 2))
          done

          echo "Failed to push test performance updates after retries." >&2
          exit 1

      - name: Upload test performance artifacts
        if: steps.gate.outputs.run_agent == 'true' && always()
        uses: actions/upload-artifact@v7
        with:
          name: test-performance-agent-${{ github.run_id }}
          path: .artifacts/test-perf/
          if-no-files-found: ignore
          retention-days: 14
