name: Test and Lint

on:
  workflow_call:
    inputs:
      ref:
        required: true
        type: string

jobs:
  test-lint:
    name: Test and Lint
    runs-on: ubuntu-latest
    # The suite is tightly distributed - 19-27 min over the last 20 successful
    # runs, max 26.5 - so 45 is ~1.7x the observed ceiling and costs a healthy
    # run nothing. Without it this job inherits the 6-hour default, which is how
    # a test that had already printed its verdict at 18 min went on to hold the
    # runner for another 82 (#2239).
    timeout-minutes: 45
    permissions:
      contents: read
    steps:
      - name: Checkout code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
        with:
          ref: ${{ inputs.ref }}
          # Need history so we can diff the changed paths against the base.
          fetch-depth: 0
          persist-credentials: false

      - name: Set up Python
        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065  # v5.6.0
        with:
          python-version: '3.12'
          cache: 'pip'

      - name: Install system dependencies (OpenGL for MuJoCo)
        run: |
          # The GitHub runner image preconfigures third-party apt repos (e.g.
          # the packages.microsoft.com azure-cli feed) whose mirrors sometimes
          # serve a corrupt InRelease file ("Clearsigned file isn't valid, got
          # 'NOSPLIT'"), making a plain `apt-get update` exit 100 and fail the
          # whole build. The packages we actually need (libosmesa6-dev, ffmpeg)
          # come from the standard Ubuntu archive, so retry a few times with a
          # short backoff to ride out transient mirror hiccups before failing.
          for attempt in 1 2 3; do
            if sudo apt-get update; then
              break
            fi
            echo "apt-get update failed (attempt ${attempt}/3), retrying in 5s..."
            sleep 5
          done
          sudo apt-get install -y libosmesa6-dev ffmpeg

      # The Device Connect integration (strands_robots/device_connect) can
      # depend on changes in the device-connect-edge / agent-tools packages
      # that are not yet on PyPI. When this PR touches the integration, install
      # those packages from source (DEVICE_CONNECT_SOURCE_REF) so CI exercises
      # the matching version instead of the published one.
      - name: Detect Device Connect integration changes
        id: dc_changed
        run: |
          BASE="origin/${{ github.base_ref || 'main' }}"
          git fetch origin "${{ github.base_ref || 'main' }}" --depth=1 || true
          if git diff --name-only "$BASE"...HEAD 2>/dev/null | grep -q '^strands_robots/device_connect/'; then
            echo "changed=true" >> "$GITHUB_OUTPUT"
            echo "Device Connect integration changed -> will install device-connect from source"
          else
            echo "changed=false" >> "$GITHUB_OUTPUT"
            echo "No Device Connect integration changes -> using published device-connect-edge"
          fi

      - name: Pin device-connect to source (integration changed)
        if: steps.dc_changed.outputs.changed == 'true'
        env:
          # Source of truth for the device-connect packages while the
          # corresponding device-connect PR is unreleased. Update the ref (or
          # repo) here to point at the branch/tag that carries the matching
          # changes; revert to the published wheel once it ships to PyPI.
          DEVICE_CONNECT_REPO: ${{ vars.DEVICE_CONNECT_REPO || 'arm/device-connect' }}
          DEVICE_CONNECT_SOURCE_REF: ${{ vars.DEVICE_CONNECT_SOURCE_REF || 'main' }}
        run: |
          # The hatch test env is an ISOLATED uv venv, so an outer `pip install`
          # would not reach it. Instead we write a uv override file: uv MERGES
          # UV_OVERRIDE entries with the pyproject [tool.uv] override-dependencies
          # (torch pins), so this redirects ONLY the device-connect packages to
          # source without disturbing anything else.
          cat > "${GITHUB_WORKSPACE}/dc-source-override.txt" <<EOF
          device-connect-edge @ git+https://github.com/${DEVICE_CONNECT_REPO}.git@${DEVICE_CONNECT_SOURCE_REF}#subdirectory=packages/device-connect-edge
          device-connect-agent-tools @ git+https://github.com/${DEVICE_CONNECT_REPO}.git@${DEVICE_CONNECT_SOURCE_REF}#subdirectory=packages/device-connect-agent-tools
          EOF
          echo "UV_OVERRIDE=${GITHUB_WORKSPACE}/dc-source-override.txt" >> "$GITHUB_ENV"
          echo "Pinned device-connect packages to ${DEVICE_CONNECT_REPO}@${DEVICE_CONNECT_SOURCE_REF}"

      - name: Install dependencies
        env:
          UV_TORCH_BACKEND: cpu
        run: |
          pip install --no-cache-dir hatch
          pip install --no-cache-dir -e ".[all,dev]"

      - name: Run lint
        env:
          UV_TORCH_BACKEND: cpu
        run: hatch run lint

      - name: Run tests
        env:
          MUJOCO_GL: osmesa
          # x86 CI is CPU-only. Pin the PyTorch backend to CPU so uv does not
          # auto-detect a CUDA driver stub (libcuda1 ships with the ffmpeg apt
          # deps) and resolve a CUDA torch wheel that cannot import on this
          # GPU-less runner -- which would silently trigger the numpy torch
          # mock in tests/conftest.py and break torch-dependent tests.
          UV_TORCH_BACKEND: cpu
        run: |
          # Diagnose the torch the hatch test env actually resolved BEFORE
          # running the suite, so a mock fallback is obvious in the log.
          hatch run python -c "import torch, sys; print('TORCH_DIAG file=%s version=%s mock=%s' % (getattr(torch,'__file__','?'), getattr(torch,'__version__','MOCK-NO-VERSION'), not hasattr(torch,'__version__')), file=sys.stderr)" || true
          hatch run test -x --strict-markers