name: Heavy Tests

# Heavy ops-shape tests under tests/heavy/. Cost minutes per run; NOT part
# of default PR CI. Two triggers:
#   - Nightly schedule (catches regressions within 24h of merge to master).
#   - On-demand opt-in via PR label `heavy-tests` (slow loop kept off by default).
#   - Manual workflow_dispatch for triage.
#
# See CLAUDE.md "tests/heavy/*.sh" entry and tests/heavy/README.md.

on:
  schedule:
    - cron: '17 8 * * *'  # 08:17 UTC daily — staggered to avoid noisy slots
  pull_request:
    # `synchronize` + `reopened` fire on subsequent pushes / reopens — without
    # them, a PR labeled `heavy-tests` would NEVER re-run heavy on later
    # commits. The job-level `if:` below filters to PRs that still carry the
    # label so we don't fan out on unrelated label changes.
    types: [labeled, synchronize, reopened]
  workflow_dispatch:

permissions:
  contents: read

# When a PR gets the heavy-tests label, cancel any in-flight heavy-tests run on
# the same ref so we only ever measure the latest commit.
concurrency:
  group: heavy-tests-${{ github.ref }}
  cancel-in-progress: true

jobs:
  heavy:
    name: Heavy tests
    # On pull_request: only run when the PR currently carries the `heavy-tests`
    # label. Works for all three trigger types (labeled, synchronize, reopened)
    # because `contains(labels.*.name, ...)` reads the live label set, not the
    # event payload's `label.name` (which is only populated for `labeled`).
    if: |
      github.event_name != 'pull_request' ||
      contains(github.event.pull_request.labels.*.name, 'heavy-tests')
    runs-on: ubuntu-latest
    timeout-minutes: 30
    services:
      postgres:
        image: pgvector/pgvector:pg16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: gbrain_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262  # v4
      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6  # v2
        with:
          bun-version: 1.3.13
      - run: bun install

      - name: Run heavy tests
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
        run: bun run test:heavy

      # The heavy runner writes per-script logs to ~/.gbrain/audit/ on every
      # run. Upload those + the rss workload JSON on failure for triage
      # without re-running locally.
      #
      # actions/upload-artifact runs as a node action — `~` is NOT expanded by
      # the shell here. Stage logs into the workspace first, then upload from
      # the stable workspace-relative path.
      - name: Stage heavy-test logs into workspace
        if: always()
        run: |
          mkdir -p heavy-artifacts
          cp -r "$HOME/.gbrain/audit"/heavy-* heavy-artifacts/ 2>/dev/null || true
          cp tests/heavy/rss-baseline.json heavy-artifacts/ 2>/dev/null || true
      - name: Upload heavy-test artifacts
        if: always()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4
        with:
          name: heavy-tests-${{ github.run_id }}-${{ github.run_attempt }}
          path: heavy-artifacts/
          retention-days: 14
          if-no-files-found: ignore
