name: E2E (Cursor)

# Live end-to-end check: drive the real `cursor-agent` CLI through oneharness and
# assert the JSON contract (scripts/e2e-cursor.sh). Unlike ci.yml this needs
# network, a model call, and the CURSOR_API_KEY secret, so it lives in its own
# workflow gated to trusted contexts only.
#
# Security: a secret must never be reachable by untrusted code. GitHub already
# withholds secrets from fork pull requests; the job `if` below is defense in
# depth — it runs only in the canonical repo and never for a PR whose head is a
# fork, so this workflow can be edited in a fork PR without exposing the key.

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: e2e-cursor-${{ github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always
  # In CI the harness is installed and auth verified up front, so any e2e skip
  # means detection/install/spawn broke (e.g. an unresolved .cmd shim on Windows)
  # — fail instead of going green having tested nothing. See scripts/e2e-lib.sh.
  OH_E2E_NO_SKIP: 1

jobs:
  live:
    name: cursor live e2e (${{ matrix.os }})
    if: >-
      github.repository == 'nickderobertis/oneharness' &&
      (github.event_name != 'pull_request' ||
       github.event.pull_request.head.repo.full_name == github.repository)
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    defaults:
      run:
        shell: bash
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4

      - name: Verify the API key is present
        env:
          CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
        run: |
          if [ -z "${CURSOR_API_KEY:-}" ]; then
            echo "::error::CURSOR_API_KEY secret is not set. Generate a user key at https://cursor.com/dashboard/api, then sync it (just secrets-sync) or set it: gh secret set CURSOR_API_KEY --repo ${{ github.repository }}"
            exit 1
          fi

      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: taiki-e/install-action@v2
        with:
          tool: just

      - name: Install Cursor CLI (Unix)
        if: runner.os != 'Windows'
        run: |
          curl https://cursor.com/install -fsS | bash
          if [ -x "$HOME/.local/bin/cursor-agent" ]; then
            echo "$HOME/.local/bin" >> "$GITHUB_PATH"
          else
            dir="$(dirname "$(find "$HOME" -name cursor-agent -type f 2>/dev/null | head -1)")"
            [ -n "$dir" ] && echo "$dir" >> "$GITHUB_PATH"
          fi

      # Windows has no curl|bash path; Cursor ships a PowerShell installer
      # (?win32=true). It drops cursor-agent under %USERPROFILE%\.local\bin —
      # put that on PATH so the bash-default steps below resolve it.
      - name: Install Cursor CLI (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $ErrorActionPreference = 'Continue'
          # Capture the installer's own output/errors: it prints "Happy coding!"
          # even when it lays down no binary, so run it visibly and inspect the
          # documented install root (%LOCALAPPDATA%\cursor-agent) directly.
          $script = irm 'https://cursor.com/install?win32=true'
          Write-Host "=== installer script: $($script.Length) chars; head ==="
          Write-Host ($script.Substring(0, [Math]::Min(800, $script.Length)))
          Write-Host "=== running installer ==="
          try { Invoke-Expression $script 2>&1 | Out-String | Write-Host } catch { Write-Host "installer threw: $_" }
          Write-Host "LASTEXITCODE=$LASTEXITCODE"
          $base = Join-Path $env:LOCALAPPDATA 'cursor-agent'
          Write-Host "=== tree of $base ==="
          if (Test-Path $base) {
            Get-ChildItem -Recurse $base -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_.FullName }
          } else { Write-Host "$base MISSING" }
          # Pick up the launcher wherever it landed (.exe, or a .cmd/.ps1 shim,
          # or the agent.exe alias). oneharness resolves and spawns a .cmd shim.
          $hit = Get-ChildItem -Path $env:LOCALAPPDATA, $env:USERPROFILE -Recurse `
            -Include 'cursor-agent.exe','cursor-agent.cmd','cursor-agent','agent.exe' -File `
            -ErrorAction SilentlyContinue | Select-Object -First 1
          if ($hit) {
            Write-Host "binary: $($hit.FullName)"
            Add-Content -Path $env:GITHUB_PATH -Value $hit.DirectoryName
          } else {
            Write-Host "::warning::no cursor-agent launcher found after install; listing roots"
            Get-ChildItem $env:LOCALAPPDATA -ErrorAction SilentlyContinue | Select-Object Name | Out-String | Write-Host
          }

      # Fail loudly if the CLI isn't runnable — a missing CLI must not read as a
      # silent pass. On Windows the launcher is cursor-agent.cmd; bash won't
      # resolve a bare name to a .cmd, but oneharness does (PATHEXT-aware `which`)
      # when it spawns it, so verify the .cmd explicitly here.
      - name: Show cursor-agent version
        run: |
          if [ "$RUNNER_OS" = Windows ]; then
            cursor-agent.cmd --version
          else
            cursor-agent --version
          fi

      - name: Live e2e against the real CLI
        env:
          CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
          CURSOR_E2E_MODEL: ${{ vars.CURSOR_E2E_MODEL }}
        run: just live-cursor
