Add bats test suite for git-manager and obsidian-vault-kb
Isolated per-test $HOME sandboxing via tests/helpers/common.bash. Covers both plugins' wrapper scripts, their setup.sh, and git-manager's two PreToolUse hooks (53 tests). Verified the suite catches regressions by temporarily reintroducing the recently-fixed vault_search.sh -g/-- ordering bug and confirming it fails.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bats
|
||||
# Tests for skills/git-manager/scripts/git_cmd.sh + _lib.sh.
|
||||
|
||||
load '../helpers/common'
|
||||
|
||||
setup() {
|
||||
sandbox_home
|
||||
SCRIPT="$REPO_ROOT/skills/git-manager/scripts/git_cmd.sh"
|
||||
CONFIG_FILE="$HOME/.agent-skills/git-manager/config.json"
|
||||
REPO="$BATS_TEST_TMPDIR/repo"
|
||||
make_test_repo "$REPO"
|
||||
}
|
||||
|
||||
# --- subcommand allowlist ---
|
||||
|
||||
@test "allowed subcommand (status) runs and returns real git output" {
|
||||
write_git_manager_config "$CONFIG_FILE" "myrepo=$REPO"
|
||||
run "$SCRIPT" status --repo myrepo
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"nothing to commit"* ]]
|
||||
}
|
||||
|
||||
@test "disallowed subcommand (stash) is rejected before git ever runs" {
|
||||
write_git_manager_config "$CONFIG_FILE" "myrepo=$REPO"
|
||||
run "$SCRIPT" stash --repo myrepo
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"is not allowed"* ]]
|
||||
}
|
||||
|
||||
@test "no subcommand at all prints usage and exits 1" {
|
||||
run "$SCRIPT"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Usage:"* ]]
|
||||
}
|
||||
|
||||
# --- repo resolution ---
|
||||
|
||||
@test "no config file at all errors" {
|
||||
run "$SCRIPT" status
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"no configuration found"* ]]
|
||||
}
|
||||
|
||||
@test "zero repos registered errors" {
|
||||
write_git_manager_config "$CONFIG_FILE"
|
||||
run "$SCRIPT" status
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"no repos registered"* ]]
|
||||
}
|
||||
|
||||
@test "exactly one repo registered auto-resolves without --repo" {
|
||||
write_git_manager_config "$CONFIG_FILE" "myrepo=$REPO"
|
||||
run "$SCRIPT" status
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"$REPO"* ]]
|
||||
}
|
||||
|
||||
@test "multiple repos registered without --repo errors and lists names" {
|
||||
REPO2="$BATS_TEST_TMPDIR/repo2"
|
||||
make_test_repo "$REPO2"
|
||||
write_git_manager_config "$CONFIG_FILE" "myrepo=$REPO" "other=$REPO2"
|
||||
run "$SCRIPT" status
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"multiple repos configured"* ]]
|
||||
[[ "$output" == *"myrepo"* ]]
|
||||
[[ "$output" == *"other"* ]]
|
||||
}
|
||||
|
||||
@test "--repo selects the named repo among several registered" {
|
||||
REPO2="$BATS_TEST_TMPDIR/repo2"
|
||||
make_test_repo "$REPO2"
|
||||
write_git_manager_config "$CONFIG_FILE" "myrepo=$REPO" "other=$REPO2"
|
||||
run "$SCRIPT" status --repo other
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"$REPO2"* ]]
|
||||
}
|
||||
|
||||
@test "unknown --repo name errors and lists registered names" {
|
||||
write_git_manager_config "$CONFIG_FILE" "myrepo=$REPO"
|
||||
run "$SCRIPT" status --repo doesnotexist
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"no repo named 'doesnotexist'"* ]]
|
||||
[[ "$output" == *"myrepo"* ]]
|
||||
}
|
||||
|
||||
# --- canonicalize_and_check_repo ---
|
||||
|
||||
@test "registered path that no longer exists errors" {
|
||||
write_git_manager_config "$CONFIG_FILE" "gone=$BATS_TEST_TMPDIR/does-not-exist"
|
||||
run "$SCRIPT" status --repo gone
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"does not exist"* ]]
|
||||
}
|
||||
|
||||
@test "registered path that isn't a git work tree errors" {
|
||||
PLAIN_DIR="$BATS_TEST_TMPDIR/not-a-repo"
|
||||
mkdir -p "$PLAIN_DIR"
|
||||
write_git_manager_config "$CONFIG_FILE" "plain=$PLAIN_DIR"
|
||||
run "$SCRIPT" status --repo plain
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"not inside a git work tree"* ]]
|
||||
}
|
||||
|
||||
@test "a registered path nested inside the repo still resolves to the toplevel" {
|
||||
mkdir -p "$REPO/subdir"
|
||||
write_git_manager_config "$CONFIG_FILE" "nested=$REPO/subdir"
|
||||
run "$SCRIPT" status --repo nested
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"== Repo: $REPO =="* ]]
|
||||
}
|
||||
|
||||
# --- -- passthrough ---
|
||||
|
||||
@test "args after -- reach git unmodified" {
|
||||
git -C "$REPO" -c user.email=test@test -c user.name=test \
|
||||
commit -q --allow-empty -m "second"
|
||||
git -C "$REPO" -c user.email=test@test -c user.name=test \
|
||||
commit -q --allow-empty -m "third"
|
||||
write_git_manager_config "$CONFIG_FILE" "myrepo=$REPO"
|
||||
run "$SCRIPT" log --repo myrepo -- --oneline -n2
|
||||
[ "$status" -eq 0 ]
|
||||
# 2 commit lines + the script's own "== Repo ==" / "== Running ==" banner lines
|
||||
commit_lines=$(printf '%s\n' "$output" | grep -c '^[0-9a-f]\{7,\} ')
|
||||
[ "$commit_lines" -eq 2 ]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bats
|
||||
# Tests for the two optional PreToolUse hooks. Run against the *deployed*
|
||||
# copies (via a real setup.sh run into a sandboxed $HOME) rather than the
|
||||
# repo source under hooks/ directly, because force-ask-on-raw-git.sh only
|
||||
# gets its companion _lib.sh (for the "already supports" wording) once
|
||||
# deployed alongside it — exactly like real usage.
|
||||
|
||||
load '../helpers/common'
|
||||
|
||||
setup() {
|
||||
sandbox_home
|
||||
SETUP="$REPO_ROOT/vendor/claude-code/plugins/git-manager/scripts/setup.sh"
|
||||
"$SETUP" --settings-scope user --install-hook --install-raw-git-hook > /dev/null
|
||||
CHAIN_HOOK="$HOME/.agent-skills/git-manager/bin/force-ask-on-chaining.sh"
|
||||
RAWGIT_HOOK="$HOME/.agent-skills/git-manager/bin/force-ask-on-raw-git.sh"
|
||||
}
|
||||
|
||||
# --- force-ask-on-chaining.sh ---
|
||||
|
||||
@test "chaining hook: && in the command forces an ask" {
|
||||
input='{"tool_name":"Bash","tool_input":{"command":"git add -A && git commit -m x"}}'
|
||||
run "$CHAIN_HOOK" <<< "$input"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *'"permissionDecision": "ask"'* ]]
|
||||
}
|
||||
|
||||
@test "chaining hook: a clean command produces no output" {
|
||||
input='{"tool_name":"Bash","tool_input":{"command":"git status"}}'
|
||||
run "$CHAIN_HOOK" <<< "$input"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -z "$output" ]
|
||||
}
|
||||
|
||||
@test "chaining hook: non-Bash tool_name produces no output even with chaining chars" {
|
||||
input='{"tool_name":"Read","tool_input":{"command":"a && b"}}'
|
||||
run "$CHAIN_HOOK" <<< "$input"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -z "$output" ]
|
||||
}
|
||||
|
||||
# --- force-ask-on-raw-git.sh ---
|
||||
|
||||
@test "raw-git hook: git_cmd.sh invocations are exempted" {
|
||||
input='{"tool_name":"Bash","tool_input":{"command":"~/.agent-skills/git-manager/bin/git_cmd.sh status"}}'
|
||||
run "$RAWGIT_HOOK" <<< "$input"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -z "$output" ]
|
||||
}
|
||||
|
||||
@test "raw-git hook: raw git on a wrapper-supported subcommand asks with 'already supports' wording" {
|
||||
input='{"tool_name":"Bash","tool_input":{"command":"git status"}}'
|
||||
run "$RAWGIT_HOOK" <<< "$input"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *'"permissionDecision": "ask"'* ]]
|
||||
[[ "$output" == *"already supports"* ]]
|
||||
}
|
||||
|
||||
@test "raw-git hook: raw git on an unsupported subcommand asks with 'accepted fallback' wording" {
|
||||
input='{"tool_name":"Bash","tool_input":{"command":"git stash"}}'
|
||||
run "$RAWGIT_HOOK" <<< "$input"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *'"permissionDecision": "ask"'* ]]
|
||||
[[ "$output" == *"accepted fallback"* ]]
|
||||
}
|
||||
|
||||
@test "raw-git hook: a non-git command produces no output" {
|
||||
input='{"tool_name":"Bash","tool_input":{"command":"ls -la"}}'
|
||||
run "$RAWGIT_HOOK" <<< "$input"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -z "$output" ]
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bats
|
||||
# Tests for vendor/claude-code/plugins/git-manager/scripts/setup.sh.
|
||||
|
||||
load '../helpers/common'
|
||||
|
||||
setup() {
|
||||
sandbox_home
|
||||
SETUP="$REPO_ROOT/vendor/claude-code/plugins/git-manager/scripts/setup.sh"
|
||||
SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
CONFIG_FILE="$HOME/.agent-skills/git-manager/config.json"
|
||||
REPO="$BATS_TEST_TMPDIR/repo"
|
||||
make_test_repo "$REPO"
|
||||
}
|
||||
|
||||
@test "writes the two expected tilde-form permission rules" {
|
||||
run "$SETUP" --settings-scope user
|
||||
[ "$status" -eq 0 ]
|
||||
allow=$(json_get "$SETTINGS_FILE" "permissions.allow")
|
||||
[[ "$allow" == *'Bash(~/.agent-skills/git-manager/bin/git_cmd.sh:*)'* ]]
|
||||
[[ "$allow" == *'Read(~/.agent-skills/git-manager/config.json)'* ]]
|
||||
}
|
||||
|
||||
@test "merges into an existing settings.json without disturbing unrelated keys" {
|
||||
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
||||
cat > "$SETTINGS_FILE" << 'EOF'
|
||||
{"permissions": {"allow": ["Bash(ls:*)"]}, "unrelatedTopLevelKey": "keep-me"}
|
||||
EOF
|
||||
run "$SETUP" --settings-scope user
|
||||
[ "$status" -eq 0 ]
|
||||
allow=$(json_get "$SETTINGS_FILE" "permissions.allow")
|
||||
[[ "$allow" == *'Bash(ls:*)'* ]]
|
||||
[[ "$allow" == *'Bash(~/.agent-skills/git-manager/bin/git_cmd.sh:*)'* ]]
|
||||
kept=$(json_get "$SETTINGS_FILE" "unrelatedTopLevelKey")
|
||||
[ "$kept" = '"keep-me"' ]
|
||||
}
|
||||
|
||||
@test "re-running setup is idempotent: no duplicate allow entries" {
|
||||
"$SETUP" --settings-scope user > /dev/null
|
||||
run "$SETUP" --settings-scope user
|
||||
[ "$status" -eq 0 ]
|
||||
count=$(python3 -c "import json; print(json.load(open('$SETTINGS_FILE'))['permissions']['allow'].count('Bash(~/.agent-skills/git-manager/bin/git_cmd.sh:*)'))")
|
||||
[ "$count" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "--repo registers a new repo in config.json" {
|
||||
run "$SETUP" --settings-scope user --repo myrepo="$REPO"
|
||||
[ "$status" -eq 0 ]
|
||||
path=$(json_get "$CONFIG_FILE" "repos.0.path")
|
||||
[ "$path" = "\"$REPO\"" ]
|
||||
}
|
||||
|
||||
@test "registering the same repo name twice updates the path instead of duplicating" {
|
||||
REPO2="$BATS_TEST_TMPDIR/repo2"
|
||||
make_test_repo "$REPO2"
|
||||
"$SETUP" --settings-scope user --repo myrepo="$REPO" > /dev/null
|
||||
run "$SETUP" --settings-scope user --repo myrepo="$REPO2"
|
||||
[ "$status" -eq 0 ]
|
||||
count=$(python3 -c "import json; print(len(json.load(open('$CONFIG_FILE'))['repos']))")
|
||||
[ "$count" -eq 1 ]
|
||||
path=$(json_get "$CONFIG_FILE" "repos.0.path")
|
||||
[ "$path" = "\"$REPO2\"" ]
|
||||
}
|
||||
|
||||
@test "without --install-hook or --install-raw-git-hook, no PreToolUse hooks are installed" {
|
||||
run "$SETUP" --settings-scope user
|
||||
[ "$status" -eq 0 ]
|
||||
run python3 -c "import json; print('hooks' in json.load(open('$SETTINGS_FILE')))"
|
||||
[ "$output" = "False" ]
|
||||
}
|
||||
|
||||
@test "--install-hook installs only the anti-chaining hook" {
|
||||
run "$SETUP" --settings-scope user --install-hook
|
||||
[ "$status" -eq 0 ]
|
||||
run python3 -c "
|
||||
import json
|
||||
h = json.load(open('$SETTINGS_FILE'))['hooks']['PreToolUse']
|
||||
cmds = [c['command'] for e in h for c in e['hooks']]
|
||||
print(len(cmds))
|
||||
print(any('force-ask-on-chaining.sh' in c for c in cmds))
|
||||
print(any('force-ask-on-raw-git.sh' in c for c in cmds))
|
||||
"
|
||||
[[ "$output" == $'1\nTrue\nFalse' ]]
|
||||
}
|
||||
|
||||
@test "--install-raw-git-hook installs only the raw-git nudge hook" {
|
||||
run "$SETUP" --settings-scope user --install-raw-git-hook
|
||||
[ "$status" -eq 0 ]
|
||||
run python3 -c "
|
||||
import json
|
||||
h = json.load(open('$SETTINGS_FILE'))['hooks']['PreToolUse']
|
||||
cmds = [c['command'] for e in h for c in e['hooks']]
|
||||
print(len(cmds))
|
||||
print(any('force-ask-on-chaining.sh' in c for c in cmds))
|
||||
print(any('force-ask-on-raw-git.sh' in c for c in cmds))
|
||||
"
|
||||
[[ "$output" == $'1\nFalse\nTrue' ]]
|
||||
}
|
||||
|
||||
@test "--settings-scope project writes under <project-dir>/.claude/settings.json" {
|
||||
PROJECT_DIR="$BATS_TEST_TMPDIR/someproject"
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
run "$SETUP" --settings-scope project --project-dir "$PROJECT_DIR"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -f "$PROJECT_DIR/.claude/settings.json" ]
|
||||
[ ! -e "$HOME/.claude/settings.json" ]
|
||||
}
|
||||
Reference in New Issue
Block a user