name: CI

on:
  push:
  pull_request:

permissions:
  contents: read

# Cancel in-progress duplicate runs on PR pushes. On main-branch pushes,
# let runs complete so we always have a full CI record.
concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: ${{ matrix.os }}
    # Issue #654 (M-82): matrix entries flagged `experimental` (e.g. 3.14)
    # don't gate the merge; all stable entries default to false and remain
    # required.
    continue-on-error: ${{ matrix.experimental || false }}
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.12"]
        include:
          - os: ubuntu-latest
            python-version: "3.10"
          - os: ubuntu-latest
            python-version: "3.11"
          - os: ubuntu-latest
            python-version: "3.13"
          # Issue #654 (M-82): Python 3.14 is the homebrew default but was
          # untested. Added non-gating (continue-on-error) for now: 3.14 may
          # surface the known test_upgrade_path UnicodeDecodeError + gate_eval
          # issues that are environment/version-specific, and we do NOT skip
          # real tests just to make 3.14 green. Promote to required once 3.14
          # is clean.
          - os: ubuntu-latest
            python-version: "3.14"
            experimental: true
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
          cache-dependency-path: pyproject.toml
      - name: Install dependencies
        run: pip install -e ".[all,dev]"
      - name: Run tests (gate — deterministic, no network)
        # Issue #426: exclude `network`-marked tests (real HuggingFace model
        # downloads) from the gate so a transient HfHubHTTPError can't fail CI
        # or block a release. Coverage is preserved by the non-gating
        # network-tests job below.
        env:
          HF_HUB_OFFLINE: "1"
          TRANSFORMERS_OFFLINE: "1"
        run: pytest tests/ -v -m "not network"
      - name: Lint
        run: ruff check truememory/ tests/

  # Issue #426 / #654 (M-52): model-download tests run here, isolated from the
  # gate. This job exercises real HuggingFace downloads + the real embedding
  # path (test_issue_654_real_vector_smoke), so the L2-vs-cosine class of bug
  # can no longer merge green.
  #
  # Previously this job was `continue-on-error: true`, which silently swallowed
  # real-vector regressions (this is how the M-03 cosine bug shipped). It is now
  # gating, but with a 3x retry loop so a transient HfHubHTTPError (network
  # flake, not a code regression) does not block PRs or releases. Only a
  # genuine, reproducible failure across all attempts fails CI.
  network-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
      - name: Set up Python 3.12
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.12"
          cache: 'pip'
          cache-dependency-path: pyproject.toml
      - name: Install dependencies
        run: pip install -e ".[all,dev]"
      - name: Run network-marked tests (retried for transient HF flakes)
        # This job MUST run online -- its whole purpose is real HuggingFace
        # downloads + the real-embedding smoke test (#654/M-52). Force offline
        # mode OFF: two test modules call os.environ.setdefault("HF_HUB_OFFLINE",
        # "1") at import, and pytest imports every module during collection, so
        # that default leaked offline-mode into this job and made the network
        # tests fail 100% deterministically (LocalEntryNotFoundError), silently
        # turning the real-vector gate into a no-op. Setting it "0" here makes
        # the setdefault a no-op so downloads actually happen.
        env:
          HF_HUB_OFFLINE: "0"
          TRANSFORMERS_OFFLINE: "0"
        run: |
          for attempt in 1 2 3; do
            echo "::group::network-tests attempt $attempt"
            if pytest tests/ -v -m "network"; then
              echo "::endgroup::"
              exit 0
            fi
            echo "::endgroup::"
            echo "network-tests attempt $attempt failed; retrying after backoff..."
            sleep $((attempt * 15))
          done
          echo "network-tests failed after 3 attempts — treating as a real regression."
          exit 1

  # Hunter F26: catch packaging regressions (missing py.typed, metadata
  # errors, broken README rendering, sdist contents) in CI instead of
  # at release time. Runs once on 3.12 only — duplicating across the
  # matrix wastes CI minutes without additional signal.
  build-check:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
      - name: Set up Python 3.12
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.12"
          cache: 'pip'
          cache-dependency-path: pyproject.toml
      - name: Install build tooling
        run: pip install build twine
      - name: Build sdist + wheel
        run: python -m build
      - name: Verify package metadata
        run: twine check --strict dist/*
      # Issue #654 (M-83): actually install the built wheel and exercise the
      # console entry point, so a broken/missing entry-point or import-time
      # failure is caught here (not just at release).
      - name: Install wheel and smoke-test console script
        run: |
          pip install dist/*.whl
          truememory-mcp --help
