diff --git a/.github/workflows/repo-contract.yml b/.github/workflows/repo-contract.yml new file mode 100644 index 0000000..89d5597 --- /dev/null +++ b/.github/workflows/repo-contract.yml @@ -0,0 +1,25 @@ +name: repo-contract + +# The local pre-commit hook can be skipped (a fresh clone has no hooks), so the +# contract is enforced on push too. Mirrors md-naming.yml. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + repo-contract: + name: repo contract + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check out the shared gate + uses: actions/checkout@v4 + with: + repository: kuhyx/utils + path: .utils + + - name: Enforce the repo contract + run: bash .utils/scripts/check_repo_contract.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3da0401..5dc0a09 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -249,3 +249,10 @@ repos: pass_filenames: false always_run: true stages: [pre-push] + + - id: repo-contract + name: repo contract (CLAUDE.md Commands + test_changed) + entry: scripts/check_repo_contract.sh + language: system + pass_filenames: false + files: ^(CLAUDE\.md|AGENTS\.md|scripts/test_changed\.sh)$ diff --git a/AGENTS.md b/AGENTS.md index 2a326ae..c5535c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,3 +14,12 @@ After running tests fix all coverage gaps and issues, do not ignore unless speci - cmd_done completion path can pick next game from snapshot-only hours; keep it aligned with HLTB cache/refresh before pick_next_game to avoid prologue-derived stale times (e.g., A Space 0.56h while cache has ~20h). - HLTB renames games (e.g., "Needy Streamer Overload" → "NEEDY GIRL OVERDOSE"). The old name lives in `game_alias`. Both `game_name` and `game_alias` must be checked when matching — fixed in `_pick_best_hltb_entry`. - **ALWAYS clear HLTB cache and re-run `run.sh` after changing the HLTB picking/matching algorithm.** Delete `~/.config/steam_backlog_enforcer/hltb_cache.json` (entire file, not just one entry) so all games get re-matched with the new logic. Then run `./run.sh` to verify correct results. Stale cache entries from the old algorithm will persist and hide bugs otherwise. + +## Commands + +- run: `./run.sh` +- test: `python3 -m pytest -q` +- test-changed: `scripts/test_changed.sh` +- lint: `pre-commit run ruff --all-files` +- coverage: `python3 -m pytest -q` +- coverage-gaps: `coverage-gaps coverage.lcov` diff --git a/scripts/check_repo_contract.sh b/scripts/check_repo_contract.sh new file mode 100755 index 0000000..8488858 --- /dev/null +++ b/scripts/check_repo_contract.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# ============================================================================ +# Fail if this repo breaks the shared repo contract. +# +# Thin delegate to the shared gate in ~/src/utils, which owns the contract +# text. Copy this file to scripts/check_repo_contract.sh in a repo; never copy +# the shared gate itself, or the shim would exec itself forever. +# ============================================================================ + +set -euo pipefail + +readonly SHARED_GATE="${UTILS_ROOT:-$HOME/src/utils}/scripts/check_repo_contract.sh" + +main() { + if [[ ! -x "$SHARED_GATE" ]]; then + echo "Error: shared repo-contract gate not found at $SHARED_GATE" >&2 + echo " Clone github.com/kuhyx/utils to ~/src/utils, or set" >&2 + echo " UTILS_ROOT to where it lives." >&2 + exit 1 + fi + + exec bash "$SHARED_GATE" "$@" +} + +main "$@" diff --git a/scripts/test_changed.sh b/scripts/test_changed.sh new file mode 100755 index 0000000..65efb87 --- /dev/null +++ b/scripts/test_changed.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# ============================================================================ +# Run only the tests related to files changed vs HEAD (staged, unstaged and +# untracked). Quiet: failures plus a one-line summary. Falls back to the full +# suite when a change cannot be mapped to tests. Generated by repo_contract. +# ============================================================================ + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" +CHANGED=() +while IFS= read -r f; do + [[ -n "$f" && -e "$f" ]] && CHANGED+=("$f") +done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u) + +pytest_run() { + if [[ -f uv.lock ]]; then uv run pytest "$@" + elif [[ -f poetry.lock ]]; then poetry run pytest "$@" + else python3 -m pytest "$@"; fi +} + +if grep -qs testmon pyproject.toml requirements*.txt; then + pytest_run --testmon -q --tb=short; exit $? +fi + +full=0 +tests=() +for f in "${CHANGED[@]}"; do + case "$f" in + */conftest.py | conftest.py | pyproject.toml | requirements*.txt | pytest.ini | setup.cfg) full=1 ;; + *.py) + base="$(basename "$f" .py)" + if [[ "$base" == test_* || "$base" == *_test ]]; then + tests+=("$f") + else + while IFS= read -r t; do tests+=("$t"); done \ + < <(git ls-files "*test_${base}.py" "*${base}_test.py") + fi + ;; + esac +done + +if [[ $full -eq 0 && ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi +if [[ $full -eq 0 && ${#tests[@]} -gt 0 ]]; then pytest_run -q --tb=short "${tests[@]}"; exit $?; fi +if [[ $full -eq 0 ]] && ! printf '%s\n' "${CHANGED[@]}" | grep -q '\.py$'; then + echo "no python changes: nothing to test"; exit 0 +fi +echo "no mapped tests: running full suite" +pytest_run -q --tb=short