# 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 — 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 = [= ...] # 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 — 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 = [= ...] 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 — makes unresolvable for the rest of the test # by building a shim directory with symlinks to every executable currently # reachable via $PATH *except* , 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 — 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" }