name: E2E Tests

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]
  schedule:
    - cron: '0 6 * * *'  # Nightly at 6am UTC
  workflow_dispatch:

permissions:
  contents: read

# Cancel a superseded run when a newer commit lands on the same PR/branch.
# PR number for pull_request events (fork-safe), github.ref fallback for
# push/scheduled runs.
concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  jsonb-parity:
    # Dedicated required guard for the JSONB double-encode bug-class (#2339).
    # PGLite parses a double-encoded jsonb string silently, so this assertion can
    # ONLY be made on real Postgres — a normal gated e2e file would skip without
    # DATABASE_URL and let the bug ship green (as #2339 did). This job provisions
    # Postgres and HARD-FAILS if DATABASE_URL is missing, so the guard can never
    # silently skip.
    name: JSONB parity (#2339 regression guard)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    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: Require DATABASE_URL (no silent skip)
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
        run: |
          if [ -z "$DATABASE_URL" ]; then
            echo "::error::DATABASE_URL must be set for the jsonb-parity job — the #2339 guard would silently skip (the exact failure PGLite hides). Failing the job." >&2
            exit 1
          fi
      - name: Run JSONB double-encode parity tests on real Postgres
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
        # --timeout also raises bun's 5s default hook budget (beforeAll/afterAll
        # do NOT inherit a test's third-arg timeout; verified on bun 1.3.x).
        # Every runner script in scripts/ passes it; bare invocations must too.
        run: bun test --timeout=60000 test/e2e/op-checkpoint-jsonb-parity.test.ts test/e2e/jsonb-roundtrip.test.ts

  tier1:
    name: Tier 1 (Mechanical)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    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 Tier 1 E2E tests
        run: bun test --timeout=60000 test/e2e/mechanical.test.ts test/e2e/mcp.test.ts
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test

  tier2:
    name: Tier 2 (LLM Skills)
    runs-on: ubuntu-latest
    # Runs on every push/PR now (promoted from schedule-only in v0.19.0).
    # Tier 1 must pass first; Tier 2 uses OPENAI_API_KEY + ANTHROPIC_API_KEY
    # from repo/org secrets. Nightly + manual triggers still supported via
    # the workflow-level `on:` list.
    needs: tier1
    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: Install OpenClaw
        # Bound + retry the install: a transient npm/registry stall here used to
        # hang unbounded and (since the v0.42.50.0 job timeout) burn the entire
        # 30m Tier 2 budget before failing — even though the install normally
        # finishes in well under a minute. `timeout` kills a hung attempt fast;
        # up to 3 attempts ride out a flaky registry. Step cap is a backstop.
        timeout-minutes: 8
        run: |
          for attempt in 1 2 3; do
            if timeout 120 npm install -g openclaw@2026.4.9; then
              exit 0
            fi
            echo "::warning::openclaw install attempt $attempt failed or timed out; retrying in 10s" >&2
            sleep 10
          done
          echo "::error::openclaw install failed after 3 attempts" >&2
          exit 1
      - name: Configure OpenClaw MCP
        run: |
          mkdir -p ~/.openclaw
          cat > ~/.openclaw/config.json << 'EOF'
          {
            "mcpServers": {
              "gbrain": {
                "command": "bun",
                "args": ["run", "src/cli.ts", "serve"],
                "env": {
                  "DATABASE_URL": "${{ env.DATABASE_URL }}"
                }
              }
            }
          }
          EOF
      - name: Run Tier 2 skill tests
        run: bun test --timeout=60000 test/e2e/skills.test.ts test/e2e/zeroentropy-live.test.ts
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          # v0.33.3.0: ZE live API tests skip gracefully when this is unset,
          # so forks without the secret stay green. The test exercises the
          # zeroEntropyCompatFetch response-rewriter + URL rewrite + flexible
          # dim handling + gateway.rerank against the real provider.
          ZEROENTROPY_API_KEY: ${{ secrets.ZEROENTROPY_API_KEY }}
