name: test-go

# Go server: vet + golangci-lint + short race tests. Runs on push to
# dev/main and PRs to main, scoped to Go-side files only — web-only or
# Flutter-only diffs don't trigger this workflow.
#
# Two jobs: `test` (fast — vet + lint + `go test -short -race`, no DB) and
# `integration` (full `go test -race` against an ephemeral Postgres).
#
# Integration-job DB wiring follows the act_runner shared-daemon pattern:
# the runner's Docker daemon also runs the operator's dev compose stack,
# so service containers get NO published ports (collision) and no
# service-name DNS. We discover the service container by the job-scoped
# name filter via the mounted docker socket and reach it by bridge IP.
# The exactly-one assertion is a hard guard — pointing tests at the dev
# Postgres would truncate it (the disaster Fable #339 exists to prevent).
#
# `web/build/` has a committed placeholder index.html so go:embed succeeds
# without needing the SPA to be freshly built. Real builds happen in
# release.yml (container) and locally during dev.

on:
  push:
    branches: [dev, main]
    paths:
      - '**/*.go'
      - 'go.mod'
      - 'go.sum'
      - 'sqlc.yaml'
      - 'internal/**'
      - 'cmd/**'
      - '.golangci.yml'
      - '.gitea/workflows/test-go.yml'

# pull_request trigger intentionally omitted — see test-web.yml for
# the rationale (single-author repo, push covers PR-merge equivalent).
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: go-ci
    container:
      image: git.fabledsword.com/bvandeusen/ci-go:1.26

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Toolchain versions
        run: |
          go version
          golangci-lint --version

      - name: go vet
        run: go vet ./...

      - name: golangci-lint
        run: golangci-lint run ./...

      - name: go test (short, race)
        run: go test -short -race ./...

  integration:
    runs-on: go-ci
    container:
      image: git.fabledsword.com/bvandeusen/ci-go:1.26
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: minstrel
          POSTGRES_PASSWORD: minstrel
          POSTGRES_DB: minstrel_test
        # No `ports:` — the runner shares the operator's dev compose
        # Docker daemon; publishing a fixed host port collides.

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Integration suite (discover service by bridge IP, migrate, test)
        run: |
          set -eux
          # Discover THIS job's Postgres service container via the
          # mounted docker socket. act_runner attaches the job
          # container and its service container(s) to a shared per-job
          # network, so scope discovery to a postgres that sits on a
          # network THIS job container is also on. The old
          # `--filter name=integration` matched EVERY concurrent
          # integration run's postgres (a dev push + the main-merge run
          # overlap → 2 candidates → false "expected exactly 1" abort).
          # The operator's dev compose `minstrel-postgres-*` is never on
          # this job's network; skip it explicitly as belt-and-suspenders
          # (a wrong target would truncate real data).
          SELF=$(cat /etc/hostname)
          SELF_NETS=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$SELF")
          test -n "$SELF_NETS"
          echo "self ($SELF) networks: $SELF_NETS"
          PG_ID=""
          PG_NAME=""
          for cid in $(docker ps --filter "ancestor=postgres:16-alpine" -q); do
            nm=$(docker inspect -f '{{.Name}}' "$cid" | sed 's#^/##')
            case "$nm" in *minstrel-postgres*|*_postgres_*) continue ;; esac
            for net in $(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$cid"); do
              case " $SELF_NETS " in *" $net "*) PG_ID="$cid"; PG_NAME="$nm"; break 2 ;; esac
            done
          done
          test -n "$PG_ID" || { echo "FATAL: no postgres service container on this job's network (self nets: $SELF_NETS)"; exit 1; }
          echo "selected postgres: $PG_ID $PG_NAME"
          PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_ID")
          test -n "$PG_IP"
          export MINSTREL_TEST_DATABASE_URL="postgres://minstrel:minstrel@${PG_IP}:5432/minstrel_test?sslmode=disable"

          # Wait for Postgres to accept TCP (no health-check dependency).
          for i in $(seq 1 60); do (echo > "/dev/tcp/${PG_IP}/5432") 2>/dev/null && break; sleep 2; done

          # Relax durability on the throwaway CI Postgres. Our test pattern
          # is dbtest.ResetDB → TRUNCATE … RESTART IDENTITY CASCADE before
          # every test, and the per-TRUNCATE commit fsync is the dominant
          # cost of the integration suite. The CI DB is rebuilt every run so
          # fsync / full_page_writes / synchronous_commit buy nothing. Apply
          # via docker exec because:
          #  - The act_runner `services:` block can't override the container
          #    command, so `postgres -c fsync=off` at boot isn't an option.
          #  - ALTER SYSTEM cannot run inside a transaction; psql -c
          #    auto-commits each statement, which is what we need.
          #  - fsync / full_page_writes are sighup GUCs and
          #    synchronous_commit is user-context, so pg_reload_conf() picks
          #    all three up with no restart.
          # Non-fatal: a perms surprise degrades to "slower", never red CI.
          docker exec "$PG_ID" psql -U minstrel -d minstrel_test \
            -c "ALTER SYSTEM SET fsync = off" \
            -c "ALTER SYSTEM SET synchronous_commit = off" \
            -c "ALTER SYSTEM SET full_page_writes = off" \
            -c "SELECT pg_reload_conf()" \
            || echo "WARN: durability relax failed; continuing"

          # Apply embedded migrations to the fresh test DB, then run the
          # full suite (no -short → integration tests execute). -p 1:
          # every integration package TRUNCATEs the one shared test DB;
          # concurrent package binaries → TRUNCATE deadlocks. Serialize
          # package execution (the documented local invocation too).
          MINSTREL_DATABASE_URL="$MINSTREL_TEST_DATABASE_URL" go run ./cmd/minstrel migrate
          go test -p 1 -race ./...
