From d169906421876db704bfe45cef6253fe519c9ee4 Mon Sep 17 00:00:00 2001 From: devantler Date: Fri, 26 Jun 2026 16:35:20 +0200 Subject: [PATCH] test(scaffold): cover the pre-commit mockery hook (run-mockery.sh) run-mockery.sh is the entry point of the pre-commit `mockery` hook; its job is the guarded no-op that keeps a fresh clone's first commit green until the project adds a mockery config, and a helpful exit-1 (not a bare `command not found`) once a config exists but mockery is not installed. It had no coverage, while its sibling rename-placeholders.sh is fully test-paired and CI-gated. Add a hermetic run-mockery.test.sh that runs the real script under a stripped PATH and pins all three branches (silent no-op without config; exit 1 + install hint when mockery absent; exec when present), and wire it into the existing template-repo-only validate-scaffold.yaml gate alongside the onboarding-script test. Docs synced in AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/scripts/run-mockery.test.sh | 92 ++++++++++++++++++++++++ .github/workflows/validate-scaffold.yaml | 21 +++--- AGENTS.md | 17 +++-- 3 files changed, 115 insertions(+), 15 deletions(-) create mode 100755 .github/scripts/run-mockery.test.sh diff --git a/.github/scripts/run-mockery.test.sh b/.github/scripts/run-mockery.test.sh new file mode 100755 index 0000000..a0c8bb2 --- /dev/null +++ b/.github/scripts/run-mockery.test.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env sh +# run-mockery.test.sh — exercise .github/scripts/run-mockery.sh end-to-end. +# +# run-mockery.sh is the `entry` of the pre-commit `mockery` hook +# (.pre-commit-config.yaml). Its whole reason to exist is the guarded no-op that +# keeps a *fresh clone's* commit green: the scaffold ships no interfaces and no +# mockery config, so the hook must do nothing until the project adds a +# `.mockery.yml`/`.mockery.yaml`, and must fail with a helpful message — not a +# bare `command not found` — once a config exists but mockery is not installed. +# A silent regression (e.g. "simplifying" it to a bare `exec mockery`) would make +# every fresh clone's first commit fail, and it has no other coverage. This test +# pins its three real behaviours: +# * no mockery config -> exit 0, silent no-op, +# * config present, mockery PATH-absent -> exit 1 with an install hint, +# * config present, mockery present -> it is exec'd (runs to completion). +# +# It runs the REAL script against throwaway working directories with a fully +# stripped PATH, so the result never depends on whatever tools the runner +# happens to have installed (the missing-mockery case is then guaranteed, not +# probabilistic). Run locally with `sh .github/scripts/run-mockery.test.sh`; CI +# runs it via .github/workflows/validate-scaffold.yaml. +set -eu + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +script="$repo_root/.github/scripts/run-mockery.sh" +# Absolute bash path: the test below runs the (bash) script under a stripped +# PATH, so it must not depend on PATH to locate the interpreter itself. +bash_bin="$(command -v bash)" + +[ -f "$script" ] || { echo "FAIL: $script not found" >&2; exit 1; } +[ -n "$bash_bin" ] || { echo "FAIL: bash not on PATH" >&2; exit 1; } + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +# --- Case 1: no mockery config -> silent no-op, exit 0 ------------------------ +work1="$tmp/no-config" +mkdir -p "$work1" +out1="$tmp/out1" +err1="$tmp/err1" +rc=0 +( cd "$work1" && PATH="" "$bash_bin" "$script" ) >"$out1" 2>"$err1" || rc=$? +[ "$rc" -eq 0 ] || fail "no-config case should exit 0, got $rc (stderr: $(cat "$err1"))" +[ -s "$out1" ] && fail "no-config case must be silent on stdout, got: $(cat "$out1")" +[ -s "$err1" ] && fail "no-config case must be silent on stderr, got: $(cat "$err1")" + +# --- Case 2: config present, mockery absent -> exit 1 with install hint ------- +work2="$tmp/config-no-mockery" +mkdir -p "$work2" +: >"$work2/.mockery.yml" +err2="$tmp/err2" +rc=0 +# PATH="" makes `command -v mockery` inside the script fail deterministically. +( cd "$work2" && PATH="" "$bash_bin" "$script" ) >/dev/null 2>"$err2" || rc=$? +[ "$rc" -eq 1 ] || fail "missing-mockery case should exit 1, got $rc" +grep -qi 'mockery not found' "$err2" || fail "missing-mockery case must explain the failure (stderr: $(cat "$err2"))" +grep -qi 'go install' "$err2" || fail "missing-mockery case must print the install hint (stderr: $(cat "$err2"))" + +# Also accept a .mockery.yaml (the script checks both extensions). +work2b="$tmp/config-yaml-no-mockery" +mkdir -p "$work2b" +: >"$work2b/.mockery.yaml" +rc=0 +( cd "$work2b" && PATH="" "$bash_bin" "$script" ) >/dev/null 2>/dev/null || rc=$? +[ "$rc" -eq 1 ] || fail ".mockery.yaml should also trigger the run path (exit 1 when mockery absent), got $rc" + +# --- Case 3: config present, mockery present -> it is exec'd ------------------- +work3="$tmp/config-with-mockery" +mkdir -p "$work3" +: >"$work3/.mockery.yml" +stubbin="$tmp/bin" +mkdir -p "$stubbin" +marker="$tmp/mockery-ran" +# /bin/sh shebang (absolute) so the stub launches under the stripped PATH; it +# uses only shell built-ins, so it needs nothing else on PATH. +cat >"$stubbin/mockery" <"$marker" +exit 0 +EOF +chmod +x "$stubbin/mockery" +rc=0 +( cd "$work3" && PATH="$stubbin" "$bash_bin" "$script" ) >/dev/null 2>/dev/null || rc=$? +[ "$rc" -eq 0 ] || fail "present-mockery case should exit 0 (exec mockery), got $rc" +[ -f "$marker" ] || fail "present-mockery case must exec mockery (stub marker not written)" + +echo "PASS: run-mockery.sh (no-op without config + helpful error when mockery absent + exec when present)" diff --git a/.github/workflows/validate-scaffold.yaml b/.github/workflows/validate-scaffold.yaml index 18270e1..587d274 100644 --- a/.github/workflows/validate-scaffold.yaml +++ b/.github/workflows/validate-scaffold.yaml @@ -6,14 +6,17 @@ on: permissions: {} -# Template-repo-only scaffold-integrity gate. scripts/rename-placeholders.sh is -# the first thing a newcomer runs after `Use this template`; a silent regression -# in it would ship broken to every project created from this template, and it has -# no other coverage. This job runs ONLY in the template repo; it no-ops in -# projects created from it (the guard below is false there), mirroring how -# cd.yaml/release.yaml run only downstream. Generated projects delete the -# onboarding script and validate their own code via the injected -# validate-go-project workflow. +# Template-repo-only scaffold-integrity gate for the scaffold's two shell +# helpers, neither of which the Go gate covers: +# * scripts/rename-placeholders.sh — the first thing a newcomer runs after +# `Use this template`; a silent regression ships broken to every generated +# project. +# * .github/scripts/run-mockery.sh — the pre-commit `mockery` hook's entry +# point; a regression makes every fresh clone's first commit fail. +# This job runs ONLY in the template repo; it no-ops in projects created from it +# (the guard below is false there), mirroring how cd.yaml/release.yaml run only +# downstream. Generated projects delete the onboarding script and validate their +# own code via the injected validate-go-project workflow. jobs: validate-scaffold: name: Validate Scaffold @@ -36,3 +39,5 @@ jobs: go-version-file: go.mod - name: 🧱 Validate onboarding (rename-placeholders.sh) run: sh scripts/rename-placeholders.test.sh + - name: 🧪 Validate pre-commit mockery hook (run-mockery.sh) + run: sh .github/scripts/run-mockery.test.sh diff --git a/AGENTS.md b/AGENTS.md index 3550b78..f3e5d11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,9 +19,10 @@ need. - `cmd/`, `internal/`, `pkg/` — conventional Go layout, each kept with a `.gitkeep` placeholder for new code. - `go.mod` / `go.sum` — module definition and checksums. - `.golangci.yml` — golangci-lint v2 config (formatters + `default: all` linters, with a few opt-outs and mock-file exclusions). -- `.github/workflows/` — `ci.yaml` (required-checks aggregation on PRs/merge queue), `cd.yaml` (GoReleaser release on `v*` tags), `validate-scaffold.yaml` (template-repo-only gate that exercises the onboarding script — no-ops downstream), `release.yaml`, `sync-labels.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`. +- `.github/workflows/` — `ci.yaml` (required-checks aggregation on PRs/merge queue), `cd.yaml` (GoReleaser release on `v*` tags), `validate-scaffold.yaml` (template-repo-only gate that exercises the onboarding script and the pre-commit mockery hook — no-ops downstream), `release.yaml`, `sync-labels.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`. - `.pre-commit-config.yaml` — local pre-commit hooks: `golangci-lint-fmt` (Go formatting) and mock generation (`mockery`, via `.github/scripts/run-mockery.sh`). - `.github/scripts/run-mockery.sh` — the pre-commit mockery hook's entry point; a guarded no-op until the project adds a `.mockery.yml`/`.mockery.yaml`, then runs `mockery` (so a fresh clone's hook stays green while the generation step is already wired). +- `.github/scripts/run-mockery.test.sh` — hermetic test for the mockery hook: runs `run-mockery.sh` under a stripped PATH and asserts the three branches (silent no-op without a config, exit 1 + install hint when mockery is absent, exec when present). Run with `sh .github/scripts/run-mockery.test.sh`; CI runs it via `validate-scaffold.yaml`. - `.mega-linter.yml`, `cspell.json` — local linting/spell-checking configuration. - `scripts/rename-placeholders.sh` — one-shot onboarding: repoints the module path (`go.mod`, Go imports, README badges) to a new project's path, leaving the upstream **Use this template** links intact. - `scripts/rename-placeholders.test.sh` — end-to-end test for the onboarding script: runs it against a throwaway copy, then asserts the module repoint, the badge rewrite, the upstream-link preservation, no stray temp files, and that the renamed scaffold builds/tests. Run with `sh scripts/rename-placeholders.test.sh`; CI runs it via `validate-scaffold.yaml`. @@ -49,12 +50,14 @@ a separate, trivially-passing `CI - Required Checks` aggregator — **not** the gate — so do **not** wire `validate-go-project` into `ci.yaml`; that would double-run every job (see go-template#76, closed as invalid). -The **onboarding script** (`scripts/rename-placeholders.sh`) sits outside that Go -gate — it is shell, run once before any Go change — so it has its own template- -repo-only check, `validate-scaffold.yaml`, which runs -`scripts/rename-placeholders.test.sh`. Run that test locally (`sh -scripts/rename-placeholders.test.sh`) when touching either script; it no-ops in -generated projects (the `github.repository` guard). +The scaffold's **shell helpers** sit outside that Go gate — they are shell, not +Go — so they have their own template-repo-only check, `validate-scaffold.yaml`, +which runs both `scripts/rename-placeholders.test.sh` (the onboarding script, +run once before any Go change) and `.github/scripts/run-mockery.test.sh` (the +pre-commit mockery hook). Run the matching test locally (`sh +scripts/rename-placeholders.test.sh` / `sh .github/scripts/run-mockery.test.sh`) +when touching either script; the gate no-ops in generated projects (the +`github.repository` guard). ## Maintenance (autonomous AI assistant)