#!/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 "$@"