Files
Henner M. Kruse dc4a5c5b5e Close bats coverage gaps, add hide_command helper, wire up kcov
- tests/helpers/common.bash: hide_command hides python3/jq/rg via a
  per-directory-batched PATH shim (not per-file — that took minutes),
  making the fail-open/grep-fallback branches testable.
- 20 new tests across all 7 .bats files: unknown flags, invalid
  --settings-scope, missing python3/jq, grep fallback when rg is
  unavailable, empty/multi-vault config edge cases, a vault path that
  stops existing or isn't a directory, and the previously-untested
  tool_name != Bash branch of force-ask-on-raw-git.sh. 53 -> 73 tests.
- hooks.bats: deploy the hooks once in setup_file() (BATS_FILE_TMPDIR)
  instead of per-test (BATS_TEST_TMPDIR) -- faster, and gives kcov one
  stable path per hook to aggregate coverage against instead of a
  fragmented copy per test.
- .gitignore (first in this repo) + tests/README.md Coverage section
  documenting the kcov invocation and why --include-pattern needs three
  entries, not two.

Real, tool-measured coverage via kcov: 86.15% (255/296 lines) across
all 10 wrapper/setup/hook scripts in both plugins, replacing an earlier
manual ~68% branch-coverage estimate.
2026-08-04 21:09:07 +00:00

137 lines
4.6 KiB
Bash

# Shared helpers for this repo's bats suites. Loaded with `load
# '../helpers/common'` at the top of each .bats file.
#
# The core trick used throughout: point $HOME at a fresh
# $BATS_TEST_TMPDIR/home before each test, so every script under test that
# reads/writes ~/.agent-skills/... or ~/.claude/settings.json is fully
# sandboxed and cleaned up automatically by bats after the test — no shared
# state between tests, and never touches the real machine's config.
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# Call from a test file's setup() after exporting HOME.
sandbox_home() {
export HOME="$BATS_TEST_TMPDIR/home"
mkdir -p "$HOME"
}
# make_test_repo <dir> — git-init a throwaway repo with one commit so
# `git status`/`log`/etc have something real to operate on.
make_test_repo() {
local dir="$1"
mkdir -p "$dir"
git -C "$dir" init -q
git -C "$dir" -c user.email=test@test -c user.name=test \
commit -q --allow-empty -m "init"
}
# write_git_manager_config <config_file> <name>=<path> [<name>=<path> ...]
# Writes a git-manager config.json registering the given repos.
write_git_manager_config() {
local config_file="$1"; shift
mkdir -p "$(dirname "$config_file")"
python3 - "$config_file" "$@" << 'PYEOF'
import json, sys
config_file, pairs = sys.argv[1], sys.argv[2:]
repos = []
for p in pairs:
name, path = p.split("=", 1)
repos.append({"name": name, "path": path})
with open(config_file, "w") as f:
json.dump({"repos": repos}, f)
PYEOF
}
# make_test_vault <dir> — a small Obsidian-shaped vault: two notes with
# frontmatter tags that wikilink each other, plus .obsidian/.git noise and
# a non-markdown attachment, so exclusion behavior is exercisable.
make_test_vault() {
local dir="$1"
mkdir -p "$dir/Notes" "$dir/.obsidian" "$dir/.git"
cat > "$dir/Notes/dns-setup.md" << 'EOF'
---
tags: [networking, dns]
---
# DNS Setup
Configured pihole as the primary resolver. See [[homelab-overview]].
EOF
cat > "$dir/Notes/homelab-overview.md" << 'EOF'
---
tags: [homelab, overview]
---
# Homelab Overview
Links to [[dns-setup]] and other topics.
EOF
echo "obsidian-internal-marker" > "$dir/.obsidian/config"
echo "attachment-only-marker" > "$dir/Notes/attachment.pdf"
}
# write_vault_config <config_file> <mode> <name>=<path> [<name>=<path> ...]
write_vault_config() {
local config_file="$1"; local mode="$2"; shift 2
mkdir -p "$(dirname "$config_file")"
python3 - "$config_file" "$mode" "$@" << 'PYEOF'
import json, sys
config_file, mode, pairs = sys.argv[1], sys.argv[2], sys.argv[3:]
vaults = []
for p in pairs:
name, path = p.split("=", 1)
vaults.append({"name": name, "path": path})
with open(config_file, "w") as f:
json.dump({"vaults": vaults, "mode": mode}, f)
PYEOF
}
# hide_command <name> — makes <name> unresolvable for the rest of the test
# by building a shim directory with symlinks to every executable currently
# reachable via $PATH *except* <name>, then replacing $PATH with just that
# directory. A plain "remove one directory from $PATH" doesn't work here:
# python3/jq/rg all live in the same directory (/usr/bin) as git/sed/grep/
# realpath/etc that the scripts under test also need, so excluding that one
# directory would break everything, not just the target command. Callers
# must do any setup that itself needs the now-hidden command (e.g. writing
# a config file with write_git_manager_config/write_vault_config, both of
# which shell out to python3) *before* calling hide_command.
hide_command() {
local hidden="$1"
local shim="$BATS_TEST_TMPDIR/shim-no-$hidden"
mkdir -p "$shim"
local dir
local oldIFS="$IFS"
IFS=':'
local dirs=($PATH)
IFS="$oldIFS"
for dir in "${dirs[@]}"; do
[ -d "$dir" ] || continue
# One `ln` call per PATH directory (not per file) — forking `basename`
# and `ln` per file made this take minutes with a few thousand files
# spread across a dozen PATH dirs. `-n` skips a name already placed by
# an earlier (higher-priority) directory, matching normal PATH lookup
# order; unreadable dirs/empty globs are silenced.
ln -sn -t "$shim" "$dir"/* 2>/dev/null || true
done
rm -f "$shim/$hidden"
export PATH="$shim"
}
# json_get <file> <dotted.path> — small helper for assertions against JSON
# written by setup.sh, without needing jq as a test dependency.
json_get() {
local file="$1" path="$2"
python3 -c '
import json, sys
with open(sys.argv[1]) as f:
data = json.load(f)
for key in sys.argv[2].split("."):
if key == "":
continue
data = data[int(key)] if key.isdigit() else data[key]
print(json.dumps(data))
' "$file" "$path"
}