name: Unit suite

# Reusable workflow that runs all unit-level coverage (Linux, macOS, Windows
# cargo, Windows bash e2e). Called by both tests.yml (PR / push-to-main) and
# release.yml (tag push). Single source of truth — PR-time and release-time
# unit jobs cannot drift.
#
# The `strict` input toggles whether the currently-scoped Windows bash e2e job
# gates the caller:
#   strict=false → PR mode. Linux, macOS, and Windows cargo block; Windows bash
#     e2e remains non-blocking (continue-on-error: true).
#   strict=true  → release mode. ALL four jobs gate. A failure in any of them
#     fails the called-workflow's overall status, blocking downstream `needs:`
#     jobs in the caller (e2e, build-*, publish-*).
#
# The release pipeline runs in strict mode so we don't ship with known
# regressions on any platform. Currently-flaky tests in the strict-only path
# must be either fixed or explicitly excluded before release time, surfacing
# real bugs rather than silently masking them.

on:
  workflow_call:
    inputs:
      strict:
        description: "When true, all jobs gate the caller; when false, only Windows bash e2e is continue-on-error"
        type: boolean
        default: false

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
  # ---------------------------------------------------------------------------
  # Rust + plugin unit tests on Linux (fast inner loop)
  # ---------------------------------------------------------------------------
  unit-linux:
    name: Unit tests (Linux)
    runs-on: ubuntu-22.04
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@v5

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2

      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - uses: actions/setup-node@v5
        with:
          node-version: "24"

      - name: Install workspace deps
        run: bun install --frozen-lockfile

      - name: Build aft-bridge dist (workspace consumers depend on it)
        run: bun run --cwd packages/aft-bridge build

      - name: Build pi-plugin dist (Pi RPC tests need it)
        # tests/pi-rpc spawns real Pi processes that load
        # packages/pi-plugin/dist/index.js. Build it before bun test recurses
        # into that workspace.
        run: bun run --cwd packages/pi-plugin build

      - name: Cargo build (debug — needed by plugin e2e tests)
        run: cargo build -p agent-file-tools

      - name: Cargo test
        run: cargo test --workspace

      - name: Bun typecheck
        run: bun run typecheck

      - name: Bun lint
        run: bun run lint

      - name: Build all JS packages (release gate)
        # Only at release time: every published JS package must build. At PR
        # time, individual package builds run inside their own test scripts.
        if: inputs.strict == true
        run: bun run --filter '*' build

      - name: Bun test (all packages)
        run: bun run test:unit
        env:
          # Tests don't need real cache locations — keep them out of the runner's
          # ~/.cache to avoid cross-test pollution.
          AFT_CACHE_DIR: ${{ runner.temp }}/aft-cache

  # ---------------------------------------------------------------------------
  # Rust + plugin tests on macOS
  # Catches macOS-specific code paths: FSEvents watcher behavior (different
  # coalescing latency from inotify), /var vs /private/var symlink
  # canonicalization, broken-symlink-chain fallback, bash_background SIGTERM
  # behavior, and Apple Silicon-specific Rust compilation. The build-darwin-*
  # jobs in release.yml only run `cargo build` — this is the only place we
  # actually execute tests on macOS in CI.
  # ---------------------------------------------------------------------------
  unit-macos:
    name: Unit tests (macOS)
    runs-on: macos-latest
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@v5

      - uses: dtolnay/rust-toolchain@stable

      # Intentionally NO Swatinem/rust-cache on macOS — rust-cache restore
      # on macos-latest overwrites the just-installed ~/.cargo/bin/cargo
      # shim with stale cached bytes, producing:
      #   error: unexpected argument 'test' found
      #   Usage: rustup-init[EXE] [OPTIONS]
      # Observed in release runs 25839773442 and 25840332625. Cold cargo
      # build is ~3min — acceptable.

      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - uses: actions/setup-node@v5
        with:
          node-version: "24"

      - name: Install workspace deps
        run: bun install --frozen-lockfile

      - name: Build aft-bridge dist (workspace consumers depend on it)
        run: bun run --cwd packages/aft-bridge build

      - name: Build pi-plugin dist (Pi RPC tests need it)
        run: bun run --cwd packages/pi-plugin build

      - name: Cargo build (debug — needed by plugin e2e tests)
        run: cargo build -p agent-file-tools

      - name: Cargo test
        run: cargo test --workspace
        env:
          AFT_TEST_DIAG: "1"

      - name: Bun typecheck
        run: bun run typecheck

      - name: Bun test (all packages)
        run: bun run test:unit
        env:
          AFT_CACHE_DIR: ${{ runner.temp }}/aft-cache

  # ---------------------------------------------------------------------------
  # Rust integration tests on Windows
  # Catches platform-conditional code paths (#[cfg(target_os = "windows")])
  # and Windows process/path/signal differences. Same suite as Linux but on a
  # real Windows runner — does NOT spin up OpenCode, that's the e2e job.
  #
  # PR and release modes are blocking. Integration tests now serialize
  # filesystem paths through serde_json before writing NDJSON requests, so
  # Windows backslashes are escaped correctly instead of being parsed as JSON
  # escape sequences.
  # ---------------------------------------------------------------------------
  unit-windows-cargo:
    name: Cargo test (Windows)
    runs-on: windows-2022
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-pc-windows-msvc

      - uses: Swatinem/rust-cache@v2

      # Compile-only gate that DENIES warnings. Catches cross-target warnings
      # (e.g. unix-`cfg` code unused on Windows) that the mac/Linux clippy gates
      # structurally cannot see, without running the flaky Windows test runtime.
      # Registry deps are unaffected (cargo caps their lints to `allow`).
      - name: Cargo check (deny warnings)
        run: cargo check --workspace --all-targets
        shell: pwsh
        env:
          RUSTFLAGS: "-D warnings"

      - name: Cargo test
        run: cargo test --workspace
        shell: pwsh
        env:
          AFT_TEST_DIAG: "1"

  # ---------------------------------------------------------------------------
  # Bash permission flow on Windows
  # Scoped to bash.test.ts only: covers the runAsk + Effect runtime path on
  # Windows for OS-parity peace of mind. The runAsk fix itself is platform-
  # agnostic JavaScript semantics — Linux/macOS in the unit-linux and
  # unit-macos jobs already cover that. This Windows job is intentionally
  # scoped to the bash permission bridge; Rust integration coverage is handled
  # by unit-windows-cargo.
  #
  # Why a real bun runner on Windows: this exercises the BinaryBridge
  # spawning aft.exe over NDJSON via stdio on the Windows process model. It
  # catches a class of bugs that Linux+macOS bun + Windows native E2E miss
  # individually — Effect-runtime semantics under bun's Windows process
  # spawn implementation.
  #
  # Within bash.test.ts, tests asserting Unix-shell OUTPUT SEMANTICS (exact raw
  # byte equality, POSIX `pwd` shape, cat/grep rewrite over forward-slash paths)
  # are skipOnWindows-skipped by design: AFT's hoisted bash runs through
  # PowerShell with backslash Windows paths, those rewrite scenarios are already
  # Unix-only (crates/aft/tests/integration/bash_rewrite_test.rs), and Linux+macOS
  # cover them. The permission-flow tests this job exists for run and gate here.
  #
  # PR mode (strict=false): non-blocking.
  # Release mode (strict=true): blocking.
  # ---------------------------------------------------------------------------
  unit-windows-bash:
    name: Bash permission e2e (Windows)
    runs-on: windows-2022
    timeout-minutes: 20
    continue-on-error: ${{ inputs.strict != true }}
    steps:
      - uses: actions/checkout@v5

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2

      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - uses: actions/setup-node@v5
        with:
          node-version: "24"

      - name: Install workspace deps
        run: bun install --frozen-lockfile
        shell: pwsh

      - name: Build aft-bridge dist (workspace consumers depend on it)
        run: bun run --cwd packages/aft-bridge build

      - name: Build pi-plugin dist (Pi RPC tests need it)
        run: bun run --cwd packages/pi-plugin build
        shell: pwsh

      - name: Cargo build (debug — bash.test.ts spawns the real aft binary)
        run: cargo build -p agent-file-tools
        shell: pwsh

      - name: Bun test (bash permission flow — full bridge + plugin + Rust)
        run: bun test src/__tests__/e2e/bash.test.ts
        working-directory: packages/opencode-plugin
        shell: pwsh
        env:
          AFT_CACHE_DIR: ${{ runner.temp }}/aft-cache
