1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 20:50:06 +00:00
Files
Mole/tests/cli.bats
Tw93 7a0b4cf07e fix: move logs to ~/Library/Logs/mole, add system idle assets cleanup
- Move log files from ~/.config/mole/ to ~/Library/Logs/mole/ per
     macOS convention. Fixes #569.
   - Add safe_sudo_find_delete for /Library/Application Support/
     com.apple.idleassetsd/Customer/ screensaver videos. Closes #570.
   - Update tests to reflect new log file paths.
2026-03-15 11:38:18 +08:00

430 lines
12 KiB
Bash

#!/usr/bin/env bats
setup_file() {
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
export PROJECT_ROOT
ORIGINAL_HOME="${HOME:-}"
export ORIGINAL_HOME
# Capture real GOCACHE before HOME is replaced with a temp dir.
# Without this, go build would use $HOME/Library/Caches/go-build inside the
# temp dir (empty), causing a full cold rebuild on every test run (~6s).
ORIGINAL_GOCACHE="$(go env GOCACHE 2>/dev/null || true)"
export ORIGINAL_GOCACHE
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-cli-home.XXXXXX")"
export HOME
mkdir -p "$HOME"
# Build Go binaries from current source for JSON tests.
# Point GOPATH/GOMODCACHE/GOCACHE at the real home so go build can reuse
# the module and build caches rather than doing a cold rebuild every run.
if command -v go > /dev/null 2>&1; then
ANALYZE_BIN="$(mktemp "${TMPDIR:-/tmp}/analyze-go.XXXXXX")"
STATUS_BIN="$(mktemp "${TMPDIR:-/tmp}/status-go.XXXXXX")"
GOPATH="${ORIGINAL_HOME}/go" GOMODCACHE="${ORIGINAL_HOME}/go/pkg/mod" \
GOCACHE="${ORIGINAL_GOCACHE}" \
go build -o "$ANALYZE_BIN" "$PROJECT_ROOT/cmd/analyze" 2>/dev/null
GOPATH="${ORIGINAL_HOME}/go" GOMODCACHE="${ORIGINAL_HOME}/go/pkg/mod" \
GOCACHE="${ORIGINAL_GOCACHE}" \
go build -o "$STATUS_BIN" "$PROJECT_ROOT/cmd/status" 2>/dev/null
export ANALYZE_BIN STATUS_BIN
fi
}
teardown_file() {
rm -rf "$HOME/.config/mole"
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
rm -f "${ANALYZE_BIN:-}" "${STATUS_BIN:-}"
}
create_fake_utils() {
local dir="$1"
mkdir -p "$dir"
cat >"$dir/sudo" <<'SCRIPT'
#!/usr/bin/env bash
if [[ "$1" == "-n" || "$1" == "-v" ]]; then
exit 0
fi
exec "$@"
SCRIPT
chmod +x "$dir/sudo"
cat >"$dir/bioutil" <<'SCRIPT'
#!/usr/bin/env bash
if [[ "$1" == "-r" ]]; then
echo "Touch ID: 1"
exit 0
fi
exit 0
SCRIPT
chmod +x "$dir/bioutil"
}
setup() {
rm -rf "$HOME/.config/mole"
mkdir -p "$HOME/.config/mole"
}
@test "mole --help prints command overview" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" --help
[ "$status" -eq 0 ]
[[ "$output" == *"mo clean"* ]]
[[ "$output" == *"mo analyze"* ]]
}
@test "mole --version reports script version" {
expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')"
run env HOME="$HOME" "$PROJECT_ROOT/mole" --version
[ "$status" -eq 0 ]
[[ "$output" == *"$expected_version"* ]]
}
@test "mole --version shows nightly channel metadata" {
expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')"
mkdir -p "$HOME/.config/mole"
cat > "$HOME/.config/mole/install_channel" <<'EOF'
CHANNEL=nightly
EOF
run env HOME="$HOME" "$PROJECT_ROOT/mole" --version
[ "$status" -eq 0 ]
[[ "$output" == *"Mole version $expected_version"* ]]
[[ "$output" == *"Channel: Nightly"* ]]
}
@test "mole unknown command returns error" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command
[ "$status" -ne 0 ]
[[ "$output" == *"Unknown command: unknown-command"* ]]
}
@test "mole uninstall --whitelist returns unsupported option error" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" uninstall --whitelist
[ "$status" -ne 0 ]
[[ "$output" == *"Unknown uninstall option: --whitelist"* ]]
}
@test "show_main_menu hides update shortcut when no update notice is available" {
run bash --noprofile --norc <<'EOF'
set -euo pipefail
HOME="$(mktemp -d)"
export HOME MOLE_TEST_MODE=1 MOLE_SKIP_MAIN=1
source "$PROJECT_ROOT/mole"
show_brand_banner() { printf 'banner\n'; }
show_menu_option() { printf '%s' "$2"; }
MAIN_MENU_BANNER=""
MAIN_MENU_UPDATE_MESSAGE=""
MAIN_MENU_SHOW_UPDATE=false
show_main_menu 1 true
EOF
[ "$status" -eq 0 ]
[[ "$output" != *"U Update"* ]]
}
@test "interactive_main_menu ignores U shortcut when update notice is hidden" {
run bash --noprofile --norc <<'EOF'
set -euo pipefail
HOME="$(mktemp -d)"
export HOME MOLE_TEST_MODE=1 MOLE_SKIP_MAIN=1
source "$PROJECT_ROOT/mole"
show_brand_banner() { :; }
show_main_menu() { :; }
hide_cursor() { :; }
show_cursor() { :; }
clear() { :; }
update_mole() { echo "UPDATE_CALLED"; }
state_file="$HOME/read_key_state"
read_key() {
if [[ ! -f "$state_file" ]]; then
: > "$state_file"
echo "UPDATE"
else
echo "QUIT"
fi
}
interactive_main_menu
EOF
[ "$status" -eq 0 ]
[[ "$output" != *"UPDATE_CALLED"* ]]
}
@test "interactive_main_menu accepts U shortcut when update notice is visible" {
run bash --noprofile --norc <<'EOF'
set -euo pipefail
HOME="$(mktemp -d)"
export HOME MOLE_TEST_MODE=1 MOLE_SKIP_MAIN=1
mkdir -p "$HOME/.cache/mole"
printf 'update available\n' > "$HOME/.cache/mole/update_message"
source "$PROJECT_ROOT/mole"
show_brand_banner() { :; }
show_main_menu() { :; }
hide_cursor() { :; }
show_cursor() { :; }
clear() { :; }
update_mole() { echo "UPDATE_CALLED"; }
read_key() { echo "UPDATE"; }
interactive_main_menu
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"UPDATE_CALLED"* ]]
}
@test "touchid status reports current configuration" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status
[ "$status" -eq 0 ]
[[ "$output" == *"Touch ID"* ]]
}
@test "mo optimize command is recognized" {
run bash -c "grep -q '\"optimize\")' '$PROJECT_ROOT/mole'"
[ "$status" -eq 0 ]
}
@test "mo analyze binary is valid" {
if [[ -f "$PROJECT_ROOT/bin/analyze-go" ]]; then
[ -x "$PROJECT_ROOT/bin/analyze-go" ]
run file "$PROJECT_ROOT/bin/analyze-go"
[[ "$output" == *"Mach-O"* ]] || [[ "$output" == *"executable"* ]]
else
skip "analyze-go binary not built"
fi
}
@test "mo clean --debug creates debug log file" {
mkdir -p "$HOME/.config/mole"
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
[ "$status" -eq 0 ]
MOLE_OUTPUT="$output"
DEBUG_LOG="$HOME/Library/Logs/mole/mole_debug_session.log"
[ -f "$DEBUG_LOG" ]
run grep "Mole Debug Session" "$DEBUG_LOG"
[ "$status" -eq 0 ]
[[ "$MOLE_OUTPUT" =~ "Debug session log saved to" ]]
}
@test "mo clean without debug does not show debug log path" {
mkdir -p "$HOME/.config/mole"
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=0 "$PROJECT_ROOT/mole" clean --dry-run
[ "$status" -eq 0 ]
[[ "$output" != *"Debug session log saved to"* ]]
}
@test "mo clean --debug logs system info" {
mkdir -p "$HOME/.config/mole"
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
[ "$status" -eq 0 ]
DEBUG_LOG="$HOME/Library/Logs/mole/mole_debug_session.log"
run grep "User:" "$DEBUG_LOG"
[ "$status" -eq 0 ]
run grep "Architecture:" "$DEBUG_LOG"
[ "$status" -eq 0 ]
}
@test "touchid status reflects pam file contents" {
pam_file="$HOME/pam_test"
cat >"$pam_file" <<'EOF'
auth sufficient pam_opendirectory.so
EOF
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
[ "$status" -eq 0 ]
[[ "$output" == *"not configured"* ]]
cat >"$pam_file" <<'EOF'
auth sufficient pam_tid.so
EOF
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
[ "$status" -eq 0 ]
[[ "$output" == *"enabled"* ]]
}
@test "enable_touchid inserts pam_tid line in pam file" {
pam_file="$HOME/pam_enable"
cat >"$pam_file" <<'EOF'
auth sufficient pam_opendirectory.so
EOF
fake_bin="$HOME/fake-bin"
create_fake_utils "$fake_bin"
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" enable
[ "$status" -eq 0 ]
grep -q "pam_tid.so" "$pam_file"
[[ -f "${pam_file}.mole-backup" ]]
}
@test "disable_touchid removes pam_tid line" {
pam_file="$HOME/pam_disable"
cat >"$pam_file" <<'EOF'
auth sufficient pam_tid.so
auth sufficient pam_opendirectory.so
EOF
fake_bin="$HOME/fake-bin-disable"
create_fake_utils "$fake_bin"
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" disable
[ "$status" -eq 0 ]
run grep "pam_tid.so" "$pam_file"
[ "$status" -ne 0 ]
}
@test "touchid enable --dry-run does not modify pam file" {
pam_file="$HOME/pam_enable_dry_run"
cat >"$pam_file" <<'EOF'
auth sufficient pam_opendirectory.so
EOF
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" enable --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"DRY RUN MODE"* ]]
run grep "pam_tid.so" "$pam_file"
[ "$status" -ne 0 ]
}
# --- JSON output mode tests ---
@test "mo analyze --json outputs valid JSON with expected fields" {
if [[ ! -x "${ANALYZE_BIN:-}" ]]; then
skip "analyze binary not available (go not installed?)"
fi
run "$ANALYZE_BIN" --json /tmp
[ "$status" -eq 0 ]
# Validate it is parseable JSON
echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)"
# Check required top-level keys
echo "$output" | python3 -c "
import sys, json
data = json.load(sys.stdin)
assert 'path' in data, 'missing path'
assert 'entries' in data, 'missing entries'
assert 'total_size' in data, 'missing total_size'
assert 'total_files' in data, 'missing total_files'
assert isinstance(data['entries'], list), 'entries is not a list'
"
}
@test "mo analyze --json entries contain required fields" {
if [[ ! -x "${ANALYZE_BIN:-}" ]]; then
skip "analyze binary not available (go not installed?)"
fi
run "$ANALYZE_BIN" --json /tmp
[ "$status" -eq 0 ]
echo "$output" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for entry in data['entries']:
assert 'name' in entry, 'entry missing name'
assert 'path' in entry, 'entry missing path'
assert 'size' in entry, 'entry missing size'
assert 'is_dir' in entry, 'entry missing is_dir'
"
}
@test "mo analyze --json path reflects target directory" {
if [[ ! -x "${ANALYZE_BIN:-}" ]]; then
skip "analyze binary not available (go not installed?)"
fi
run "$ANALYZE_BIN" --json /tmp
[ "$status" -eq 0 ]
echo "$output" | python3 -c "
import sys, json
data = json.load(sys.stdin)
assert data['path'] == '/tmp' or data['path'] == '/private/tmp', \
f\"unexpected path: {data['path']}\"
"
}
@test "mo status --json outputs valid JSON with expected fields" {
if [[ ! -x "${STATUS_BIN:-}" ]]; then
skip "status binary not available (go not installed?)"
fi
run "$STATUS_BIN" --json
[ "$status" -eq 0 ]
# Validate it is parseable JSON
echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)"
# Check required top-level keys
echo "$output" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for key in ['cpu', 'memory', 'disks', 'health_score', 'host', 'uptime']:
assert key in data, f'missing key: {key}'
"
}
@test "mo status --json cpu section has expected structure" {
if [[ ! -x "${STATUS_BIN:-}" ]]; then
skip "status binary not available (go not installed?)"
fi
run "$STATUS_BIN" --json
[ "$status" -eq 0 ]
echo "$output" | python3 -c "
import sys, json
data = json.load(sys.stdin)
cpu = data['cpu']
assert 'usage' in cpu, 'cpu missing usage'
assert 'logical_cpu' in cpu, 'cpu missing logical_cpu'
assert isinstance(cpu['usage'], (int, float)), 'cpu usage is not a number'
"
}
@test "mo status --json memory section has expected structure" {
if [[ ! -x "${STATUS_BIN:-}" ]]; then
skip "status binary not available (go not installed?)"
fi
run "$STATUS_BIN" --json
[ "$status" -eq 0 ]
echo "$output" | python3 -c "
import sys, json
data = json.load(sys.stdin)
mem = data['memory']
assert 'total' in mem, 'memory missing total'
assert 'used' in mem, 'memory missing used'
assert 'used_percent' in mem, 'memory missing used_percent'
assert mem['total'] > 0, 'memory total should be positive'
"
}
@test "mo status --json piped to stdout auto-detects JSON mode" {
if [[ ! -x "${STATUS_BIN:-}" ]]; then
skip "status binary not available (go not installed?)"
fi
# When piped (not a tty), status should auto-detect and output JSON
output=$("$STATUS_BIN" 2>/dev/null)
echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)"
}