#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SETUP_SH="$REPO_ROOT/scripts/setup.sh"
UNINSTALL_SH="$REPO_ROOT/scripts/uninstall.sh"
pass() {
echo "[PASS] $1"
}
make_home() {
mktemp -d /tmp/claude-scholar-test.XXXXXX
}
run_setup() {
HOME="$1" bash "$SETUP_SH" >/dev/null
}
run_uninstall() {
HOME="$1" bash "$UNINSTALL_SH" >/dev/null
}
test_roundtrip_empty_home() {
local home
home="$(make_home)"
run_setup "$home"
test -f "$home/.claude/.claude-scholar-manifest.txt"
test -f "$home/.claude/.claude-scholar-install-state"
test -f "$home/.claude/CLAUDE.md"
run_uninstall "$home"
test ! -f "$home/.claude/.claude-scholar-manifest.txt"
test ! -f "$home/.claude/.claude-scholar-install-state"
test ! -f "$home/.claude/settings.json"
if [ -d "$home/.claude" ]; then
! find "$home/.claude"/{skills,commands,agents,rules,hooks,scripts} -type f 2>/dev/null | grep -q .
fi
pass "roundtrip on empty home"
}
test_preserve_preexisting_settings_keys() {
local home
home="$(make_home)"
mkdir -p "$home/.claude"
python3 - "$home/.claude/settings.json" <<'PY'
import json, sys
path = sys.argv[1]
data = {
"mcpServers": {
"zotero": {
"command": "custom-zotero"
}
},
"enabledPlugins": {
"superpowers@claude-plugins-official": False
},
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo user-hook",
"timeout": 3
}
]
}
]
}
}
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
PY
run_setup "$home"
run_uninstall "$home"
python3 - "$home/.claude/settings.json" <<'PY'
import json, sys
data = json.load(open(sys.argv[1], encoding="utf-8"))
assert data["mcpServers"]["zotero"]["command"] == "custom-zotero"
assert "args" not in data["mcpServers"]["zotero"]
assert "env" not in data["mcpServers"]["zotero"]
assert data["enabledPlugins"]["superpowers@claude-plugins-official"] is False
hooks = data["hooks"]["Stop"][0]["hooks"]
assert len(hooks) == 1 and hooks[0]["command"] == "echo user-hook"
PY
pass "preserve pre-existing settings entries with overlapping keys"
}
test_manifest_missing_fails_safe() {
local home
home="$(make_home)"
run_setup "$home"
rm -f "$home/.claude/.claude-scholar-manifest.txt"
if HOME="$home" bash "$UNINSTALL_SH" >/tmp/claude-scholar-uninstall-fail.log 2>&1; then
echo "[FAIL] manifest missing should fail"
cat /tmp/claude-scholar-uninstall-fail.log
exit 1
fi
test -f "$home/.claude/CLAUDE.md"
test -f "$home/.claude/.claude-scholar-install-state"
pass "manifest missing fails safely"
}
test_state_records_direct_claude_targets() {
local home
home="$(make_home)"
run_setup "$home"
python3 - "$home/.claude/.claude-scholar-install-state" <<'PY'
import json, sys
state = json.load(open(sys.argv[1], encoding="utf-8"))
targets = set(state["claudeTargets"])
assert "CLAUDE.md" in targets, targets
assert "CLAUDE.zh-CN.md" in targets, targets
PY
run_uninstall "$home"
test ! -f "$home/.claude/CLAUDE.md"
test ! -f "$home/.claude/CLAUDE.zh-CN.md"
pass "state records direct CLAUDE targets"
}
test_identical_preexisting_file_is_not_owned() {
local home
home="$(make_home)"
mkdir -p "$home/.claude/hooks"
cp "$REPO_ROOT/hooks/security-guard.js" "$home/.claude/hooks/security-guard.js"
run_setup "$home"
run_uninstall "$home"
test -f "$home/.claude/hooks/security-guard.js"
pass "identical pre-existing file is not treated as owned"
}
test_reinstall_keeps_owned_files_owned() {
local home
home="$(make_home)"
run_setup "$home"
run_setup "$home"
run_uninstall "$home"
test ! -f "$home/.claude/hooks/security-guard.js"
test ! -f "$home/.claude/CLAUDE.md"
pass "reinstall preserves ownership of previously installed files"
}
test_legacy_install_upgrade_adopts_existing_files() {
local home
home="$(make_home)"
mkdir -p "$home/.claude/hooks"
cp "$REPO_ROOT/CLAUDE.md" "$home/.claude/CLAUDE.md"
cp "$REPO_ROOT/hooks/security-guard.js" "$home/.claude/hooks/security-guard.js"
python3 - "$home/.claude/settings.json" <<'PY'
import json, sys
data = {
"mcpServers": {
"streamable-mcp-server": {
"type": "streamable-http",
"url": "http://127.0.0.1:12306/mcp"
}
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit",
"hooks": [
{
"type": "command",
"command": "node -e \"const p=require('path'),h=require('os').homedir();require('child_process').execSync('node '+p.join(h,'.claude/hooks/security-guard.js'),{stdio:'inherit'})\"",
"timeout": 5
}
]
}
]
}
}
with open(sys.argv[1], "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
PY
run_setup "$home"
run_uninstall "$home"
test ! -f "$home/.claude/CLAUDE.md"
test ! -f "$home/.claude/hooks/security-guard.js"
pass "legacy install upgrade adopts existing managed files"
}
test_overlapping_mcp_keys_do_not_trigger_legacy_adoption() {
local home
home="$(make_home)"
mkdir -p "$home/.claude/hooks"
cp "$REPO_ROOT/hooks/security-guard.js" "$home/.claude/hooks/security-guard.js"
python3 - "$home/.claude/settings.json" <<'PY'
import json, sys
data = {
"mcpServers": {
"zotero": {
"command": "custom-zotero"
}
},
"enabledPlugins": {
"superpowers@claude-plugins-official": False
}
}
with open(sys.argv[1], "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
PY
run_setup "$home"
run_uninstall "$home"
test -f "$home/.claude/hooks/security-guard.js"
pass "overlapping MCP keys alone do not trigger legacy adoption"
}
main() {
bash -n "$SETUP_SH"
bash -n "$UNINSTALL_SH"
test_roundtrip_empty_home
test_preserve_preexisting_settings_keys
test_manifest_missing_fails_safe
test_state_records_direct_claude_targets
test_identical_preexisting_file_is_not_owned
test_reinstall_keeps_owned_files_owned
test_legacy_install_upgrade_adopts_existing_files
test_overlapping_mcp_keys_do_not_trigger_legacy_adoption
}
main "$@"