#!/bin/bash # # script/test # # Functional tests for the scripts that have non-trivial logic: # stow-package (conflict parsing + backup moves), setup-git-config # (Git-native rendering, migration, and drift detection), and install-mise # (platform-aware mainline installation), and apply-macos-preferences # (platform gating, Dock/screenshot defaults, and LaunchAgent reloads). Each test runs # inside its own mktemp sandbox with a stubbed $HOME; nothing touches the real # system. # # The runner does NOT use `set -e` globally so that a failing assertion # in one test reports and continues instead of aborting the whole suite. # Individual test functions set `set -e` locally so a bare `[ ... ]` or # command failure aborts just that test with a nonzero exit code, which # run_test translates into a FAIL. set -uo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' else RED='' GREEN='' NC='' fi passed=0 failed=0 run_test() { local name=$1 local fn=$2 local sandbox sandbox=$(mktemp -d) local output if output=$("$fn" "$sandbox" 2>&1); then printf " ${GREEN}PASS${NC} %s\n" "$name" passed=$((passed + 1)) else printf " ${RED}FAIL${NC} %s\n" "$name" if [ -n "$output" ]; then printf '%s\n' "$output" | sed 's/^/ /' fi failed=$((failed + 1)) fi rm -rf "$sandbox" } # ------- stow-package ------- # # The sandbox layout mirrors the real repo just enough for stow-package's # `$(dirname "$0")/..` resolution: we copy the script into # $sandbox/repo/script/ and build a fake package next to it. Invoking # $sandbox/repo/script/stow-package makes REPO_ROOT resolve to # $sandbox/repo, so `pkg` is looked up inside the sandbox, not the real # dotfiles checkout. make_stow_sandbox_repo() { local sandbox=$1 mkdir -p "$sandbox/repo/script" cp "$REPO_ROOT/script/stow-package" "$sandbox/repo/script/" } test_stow_clean() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config" "$sandbox/home" echo "repo-content" > "$sandbox/repo/pkg/.config/foo.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ -L "$sandbox/home/.config/foo.conf" ] || { echo "expected symlink"; exit 1; } [ ! -d "$sandbox/home/.dotfiles-backup" ] || { echo "backup dir should not exist"; exit 1; } } test_stow_real_file_conflict() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config" "$sandbox/home/.config" echo "repo-content" > "$sandbox/repo/pkg/.config/foo.conf" echo "original-home-content" > "$sandbox/home/.config/foo.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ -L "$sandbox/home/.config/foo.conf" ] || { echo "expected symlink after stow"; exit 1; } local backup backup=$(find "$sandbox/home/.dotfiles-backup" -name foo.conf -print -quit 2>/dev/null) [ -n "$backup" ] || { echo "no backup file found"; exit 1; } [ "$(cat "$backup")" = "original-home-content" ] \ || { echo "backup contents not preserved"; exit 1; } } test_stow_foreign_symlink_conflict() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config" "$sandbox/home/.config" "$sandbox/other" echo "repo-content" > "$sandbox/repo/pkg/.config/foo.conf" echo "foreign" > "$sandbox/other/foreign-target" ln -s "$sandbox/other/foreign-target" "$sandbox/home/.config/foo.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ -L "$sandbox/home/.config/foo.conf" ] || { echo "expected symlink"; exit 1; } local target target=$(readlink "$sandbox/home/.config/foo.conf") case "$target" in */repo/pkg/.config/foo.conf) ;; *) echo "symlink points to wrong target: $target"; exit 1 ;; esac find "$sandbox/home/.dotfiles-backup" -name foo.conf 2>/dev/null | grep -q . \ || { echo "foreign symlink not backed up"; exit 1; } } test_stow_foreign_directory_symlink_conflict() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config/demo" \ "$sandbox/home" \ "$sandbox/other/config/demo" echo "repo-content" > "$sandbox/repo/pkg/.config/demo/foo.conf" echo "foreign-content" > "$sandbox/other/config/demo/foreign.conf" ln -s "$sandbox/other/config" "$sandbox/home/.config" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null if ! { [ -d "$sandbox/home/.config" ] && [ ! -L "$sandbox/home/.config" ]; }; then echo "foreign directory symlink was not replaced"; exit 1 fi [ -L "$sandbox/home/.config/demo/foo.conf" ] \ || { echo "package file was not stowed"; exit 1; } [ -f "$sandbox/other/config/demo/foreign.conf" ] \ || { echo "foreign directory contents were changed"; exit 1; } find "$sandbox/home/.dotfiles-backup" -type l -name .config 2>/dev/null | grep -q . \ || { echo "foreign directory symlink not backed up"; exit 1; } } test_stow_mixed_conflicts_and_clean() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config/nested" "$sandbox/home/.config/nested" echo "a" > "$sandbox/repo/pkg/.config/a.conf" echo "b" > "$sandbox/repo/pkg/.config/nested/b.conf" echo "c" > "$sandbox/repo/pkg/.config/nested/c.conf" echo "home-b" > "$sandbox/home/.config/nested/b.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ -L "$sandbox/home/.config/a.conf" ] || { echo "a not symlinked"; exit 1; } [ -L "$sandbox/home/.config/nested/b.conf" ] || { echo "b not symlinked"; exit 1; } [ -L "$sandbox/home/.config/nested/c.conf" ] || { echo "c not symlinked"; exit 1; } find "$sandbox/home/.dotfiles-backup" -name b.conf 2>/dev/null | grep -q . \ || { echo "b.conf should be in backup"; exit 1; } if find "$sandbox/home/.dotfiles-backup" -name a.conf 2>/dev/null | grep -q .; then echo "a.conf should not be in backup"; exit 1 fi if find "$sandbox/home/.dotfiles-backup" -name c.conf 2>/dev/null | grep -q .; then echo "c.conf should not be in backup"; exit 1 fi } test_stow_restow_is_idempotent() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config" "$sandbox/home" echo "repo-content" > "$sandbox/repo/pkg/.config/foo.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null # Second run: the target is already the symlink we would create, so it # must not be treated as a conflict and moved into the backup dir. HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ -L "$sandbox/home/.config/foo.conf" ] || { echo "expected symlink"; exit 1; } [ "$(cat "$sandbox/home/.config/foo.conf")" = "repo-content" ] \ || { echo "symlink no longer resolves to package content"; exit 1; } [ ! -d "$sandbox/home/.dotfiles-backup" ] \ || { echo "re-stow should not back anything up"; exit 1; } } test_stow_restow_prunes_obsolete_link() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config" "$sandbox/home" echo "keep" > "$sandbox/repo/pkg/.config/keep.conf" echo "remove" > "$sandbox/repo/pkg/.config/remove.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null rm "$sandbox/repo/pkg/.config/remove.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ -L "$sandbox/home/.config/keep.conf" ] || { echo "current link missing"; exit 1; } if [ -e "$sandbox/home/.config/remove.conf" ] || [ -L "$sandbox/home/.config/remove.conf" ]; then echo "obsolete link was not pruned"; exit 1 fi } test_stow_restow_unfolds_owned_directory() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config/demo" "$sandbox/home" echo "repo-content" > "$sandbox/repo/pkg/.config/demo/foo.conf" ( cd "$sandbox/repo" stow -t "$sandbox/home" pkg ) [ -L "$sandbox/home/.config" ] || { echo "fixture did not fold directory"; exit 1; } HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null if ! { [ -d "$sandbox/home/.config" ] && [ ! -L "$sandbox/home/.config" ]; }; then echo "owned directory symlink was not unfolded"; exit 1 fi [ -L "$sandbox/home/.config/demo/foo.conf" ] \ || { echo "package file was not restowed"; exit 1; } [ ! -d "$sandbox/home/.dotfiles-backup" ] \ || { echo "owned directory symlink should not be backed up"; exit 1; } } test_stow_local_ignore_runtime_state() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config/nvim" "$sandbox/home/.config/nvim" cp "$REPO_ROOT/common/.stow-local-ignore" "$sandbox/repo/pkg/" echo "pkg/.config/nvim/lazy-lock.json" > "$sandbox/repo/.gitignore" ( cd "$sandbox/repo" git init -q ) echo "tracked" > "$sandbox/repo/pkg/.config/nvim/init.lua" echo "source-runtime" > "$sandbox/repo/pkg/.config/nvim/lazy-lock.json" echo "host-runtime" > "$sandbox/home/.config/nvim/lazy-lock.json" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ -L "$sandbox/home/.config/nvim/init.lua" ] || { echo "tracked file not stowed"; exit 1; } [ ! -L "$sandbox/home/.config/nvim/lazy-lock.json" ] \ || { echo "runtime file should not be linked"; exit 1; } [ "$(cat "$sandbox/home/.config/nvim/lazy-lock.json")" = "host-runtime" ] \ || { echo "host runtime file was not preserved"; exit 1; } [ ! -d "$sandbox/home/.dotfiles-backup" ] \ || { echo "ignored runtime file should not be backed up"; exit 1; } } test_stow_dangling_symlink_conflict() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" mkdir -p "$sandbox/repo/pkg/.config" "$sandbox/home/.config" echo "repo-content" > "$sandbox/repo/pkg/.config/foo.conf" # A link to a target that does not exist: `test -e` is false for this, # so it is only detectable via -L. ln -s "$sandbox/nowhere/gone.conf" "$sandbox/home/.config/foo.conf" HOME="$sandbox/home" "$sandbox/repo/script/stow-package" pkg > /dev/null [ "$(cat "$sandbox/home/.config/foo.conf")" = "repo-content" ] \ || { echo "dangling link was not replaced by the package symlink"; exit 1; } find "$sandbox/home/.dotfiles-backup" -name foo.conf 2>/dev/null | grep -q . \ || { echo "dangling symlink not backed up"; exit 1; } } test_stow_missing_arg() { local sandbox=$1 if "$REPO_ROOT/script/stow-package" 2>/dev/null; then echo "expected nonzero exit for missing arg" exit 1 fi } test_stow_nonexistent_package() { local sandbox=$1 set -e make_stow_sandbox_repo "$sandbox" if HOME="$sandbox/home" "$sandbox/repo/script/stow-package" nonexistent 2>/dev/null; then echo "expected nonzero exit for missing package" exit 1 fi } # ------- setup-git-config ------- # # setup-git-config hardcodes its template path relative to $REPO_ROOT # (via $(dirname "$0")/..), so we invoke the real script from the real # repo but stub $HOME so the identity and rendered config live in the # sandbox. This tests the real template rendering end-to-end. write_test_identity() { local home=$1 email=$2 key=$3 local identity="$home/.config/local/git/identity.gitconfig" mkdir -p "$(dirname "$identity")" git config --file "$identity" user.email "$email" git config --file "$identity" user.signingkey "$key" } test_git_config_basic_render() { local sandbox=$1 set -e local key='ssh-ed25519 AAAATESTBASICKEY' write_test_identity "$sandbox/home" "test@example.com" "$key" HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null local config="$sandbox/home/.config/git/config" [ -f "$config" ] || { echo "config not rendered"; exit 1; } [ "$(git config --file "$config" --get user.email)" = "test@example.com" ] \ || { echo "email not injected"; exit 1; } [ "$(git config --file "$config" --get user.signingkey)" = "$key" ] \ || { echo "signing key not injected"; exit 1; } } test_git_config_special_values() { local sandbox=$1 set -e local email='weird&chars|here\test@example.com' local key='ssh-ed25519 AAAATESTKEY weird&chars|comment\withbackslash' write_test_identity "$sandbox/home" "$email" "$key" HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null local config="$sandbox/home/.config/git/config" [ "$(git config --file "$config" --get user.email)" = "$email" ] \ || { echo "email value did not round trip"; exit 1; } [ "$(git config --file "$config" --get user.signingkey)" = "$key" ] \ || { echo "signing key value did not round trip"; exit 1; } } test_git_config_codespaces_skip() { local sandbox=$1 set -e mkdir -p "$sandbox/home" CODESPACES=1 HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null [ -f "$sandbox/home/.config/git/config" ] \ || { echo "codespaces path should render config"; exit 1; } local config="$sandbox/home/.config/git/config" if git config --file "$config" --get-regexp '^user\.' >/dev/null; then echo "[user] section should be removed" exit 1 fi if git config --file "$config" --get-regexp '^include\.' >/dev/null; then echo "[include] section should be removed" exit 1 fi [ "$(git config --file "$config" --get commit.gpgsign)" = "false" ] \ || { echo "gpgsign should be false in codespaces mode"; exit 1; } git config --file "$config" --get alias.co >/dev/null \ || { echo "aliases should be present in codespaces config"; exit 1; } } test_git_config_missing_email() { local sandbox=$1 set -e mkdir -p "$sandbox/home/.config/local/git" git config --file "$sandbox/home/.config/local/git/identity.gitconfig" \ user.signingkey "ssh-ed25519 AAAATESTKEY" if HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" 2>/dev/null; then echo "expected nonzero exit when user.email missing" exit 1 fi } test_git_config_missing_signingkey() { local sandbox=$1 set -e mkdir -p "$sandbox/home/.config/local/git" git config --file "$sandbox/home/.config/local/git/identity.gitconfig" user.email x@y.z if HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" 2>/dev/null; then echo "expected nonzero exit when user.signingkey missing" exit 1 fi } test_git_config_rejects_non_ssh_signingkey() { local sandbox=$1 set -e write_test_identity "$sandbox/home" "test@example.com" "ABCD1234DEADBEEF" if HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" 2>/dev/null; then echo "expected nonzero exit for a non-SSH signing key" exit 1 fi } test_git_config_legacy_identity_migration() { local sandbox=$1 set -e mkdir -p "$sandbox/home/.config/dotfiles" cat > "$sandbox/home/.config/dotfiles/identity.env" <<'EOF' EMAIL=legacy@example.com SIGNINGKEY=ssh-ed25519 AAAALEGACYKEY EOF HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null local identity="$sandbox/home/.config/local/git/identity.gitconfig" [ -f "$identity" ] || { echo "legacy identity was not migrated"; exit 1; } [ "$(git config --file "$identity" --get user.email)" = "legacy@example.com" ] \ || { echo "migrated email is incorrect"; exit 1; } [ "$(git config --file "$identity" --get user.signingkey)" = "ssh-ed25519 AAAALEGACYKEY" ] \ || { echo "migrated signing key is incorrect"; exit 1; } [ -f "$sandbox/home/.config/local/git/identity.env.migrated" ] \ || { echo "legacy identity should be archived"; exit 1; } [ ! -e "$sandbox/home/.config/dotfiles/identity.env" ] \ || { echo "legacy identity should leave the old directory"; exit 1; } } test_git_config_prior_git_identity_migration() { local sandbox=$1 set -e local old_identity="$sandbox/home/.config/dotfiles/identity.gitconfig" mkdir -p "$(dirname "$old_identity")" git config --file "$old_identity" user.email prior@example.com git config --file "$old_identity" user.signingkey "ssh-ed25519 AAAAPRIORKEY" HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null local identity="$sandbox/home/.config/local/git/identity.gitconfig" [ "$(git config --file "$identity" --get user.email)" = "prior@example.com" ] \ || { echo "prior Git identity email is incorrect"; exit 1; } [ "$(git config --file "$identity" --get user.signingkey)" = "ssh-ed25519 AAAAPRIORKEY" ] \ || { echo "prior Git identity signing key is incorrect"; exit 1; } [ ! -e "$old_identity" ] || { echo "prior Git identity should be moved"; exit 1; } } test_git_config_check_detects_drift() { local sandbox=$1 set -e write_test_identity "$sandbox/home" "test@example.com" "ssh-ed25519 AAAATESTKEY" HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" --check git config --file "$sandbox/home/.config/git/config" alias.drift true if HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" --check; then echo "expected drift check to fail" exit 1 fi } TEST_SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITESTKEY" test_git_config_allowed_signers_render() { local sandbox=$1 set -e write_test_identity "$sandbox/home" "test@example.com" "$TEST_SSH_KEY" HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null local signers="$sandbox/home/.config/local/git/allowed_signers" [ -f "$signers" ] || { echo "allowed_signers not rendered"; exit 1; } [ "$(cat "$signers")" = "test@example.com $TEST_SSH_KEY" ] \ || { echo "allowed_signers entry incorrect"; exit 1; } } test_git_config_allowed_signers_extra() { local sandbox=$1 set -e write_test_identity "$sandbox/home" "test@example.com" "$TEST_SSH_KEY" echo "teammate@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOTHER" \ > "$sandbox/home/.config/local/git/allowed_signers.extra" HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null local signers="$sandbox/home/.config/local/git/allowed_signers" grep -q '^test@example.com ' "$signers" \ || { echo "own identity missing from allowed_signers"; exit 1; } grep -q '^teammate@example.com ' "$signers" \ || { echo "extra signers not appended"; exit 1; } } test_git_config_allowed_signers_codespaces() { local sandbox=$1 set -e mkdir -p "$sandbox/home" CODESPACES=1 HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null [ ! -e "$sandbox/home/.config/local/git/allowed_signers" ] \ || { echo "Codespaces should not render allowed_signers"; exit 1; } } test_git_config_check_detects_signers_drift() { local sandbox=$1 set -e write_test_identity "$sandbox/home" "test@example.com" "$TEST_SSH_KEY" HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" > /dev/null HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" --check echo "stranger@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAISTRANGER" \ >> "$sandbox/home/.config/local/git/allowed_signers" if HOME="$sandbox/home" "$REPO_ROOT/script/setup-git-config" --check; then echo "expected allowed_signers drift to fail the check" exit 1 fi } # ------- install-mise ------- make_mise_test_path() { local sandbox=$1 os=$2 mkdir -p "$sandbox/bin" cat > "$sandbox/bin/uname" < "$path" <> '$log' printf '%s\n' "\$*" >> '$log' EOF chmod +x "$path" } test_mise_installs_mainline_on_darwin() { local sandbox=$1 set -e local path log path=$(make_mise_test_path "$sandbox" Darwin) log="$sandbox/mise.log" cat > "$sandbox/bin/curl" < "\$MISE_INSTALL_PATH" <<'MISE' #!/bin/sh printf '%s\n' installed >> '$log' printf '%s\n' "\$*" >> '$log' MISE chmod +x "\$MISE_INSTALL_PATH" INSTALLER EOF chmod +x "$sandbox/bin/curl" HOME="$sandbox/home" PATH="$path" "$REPO_ROOT/script/install-mise" [ -x "$sandbox/home/.local/bin/mise" ] \ || { echo "mainline mise binary was not installed"; exit 1; } [ "$(cat "$log")" = "$(printf 'installed\ntrust\ninstalled\ninstall')" ] \ || { echo "installed mise did not configure runtimes"; exit 1; } } test_mise_skips_installer_when_mainline_exists() { local sandbox=$1 set -e local path log path=$(make_mise_test_path "$sandbox" Darwin) log="$sandbox/mise.log" write_fake_mise "$sandbox/home/.local/bin/mise" "$log" existing cat > "$sandbox/bin/curl" <<'EOF' #!/bin/sh echo "curl should not run" >&2 exit 99 EOF chmod +x "$sandbox/bin/curl" HOME="$sandbox/home" PATH="$path" "$REPO_ROOT/script/install-mise" [ "$(cat "$log")" = "$(printf 'existing\ntrust\nexisting\ninstall')" ] \ || { echo "existing mainline mise was not used"; exit 1; } } test_mise_errors_when_installer_produces_no_binary() { local sandbox=$1 set -e local path path=$(make_mise_test_path "$sandbox" Darwin) cat > "$sandbox/bin/curl" <<'EOF' #!/bin/sh printf '%s\n' 'exit 0' EOF chmod +x "$sandbox/bin/curl" if HOME="$sandbox/home" PATH="$path" "$REPO_ROOT/script/install-mise" 2>/dev/null; then echo "expected failure when installer produced no executable" exit 1 fi } test_mise_does_not_install_on_linux() { local sandbox=$1 set -e local path path=$(make_mise_test_path "$sandbox" Linux) cat > "$sandbox/bin/curl" <<'EOF' #!/bin/sh echo "curl should not run" >&2 exit 99 EOF chmod +x "$sandbox/bin/curl" HOME="$sandbox/home" PATH="$sandbox/bin" "$REPO_ROOT/script/install-mise" > "$sandbox/output" [ ! -e "$sandbox/home/.local/bin/mise" ] \ || { echo "Linux bootstrap should not install mise"; exit 1; } grep -q "mise not on PATH; skipping" "$sandbox/output" \ || { echo "missing skip message"; exit 1; } } # ------- apply-macos-preferences ------- make_macos_preferences_test_path() { local sandbox=$1 os=$2 print_status=$3 mkdir -p "$sandbox/bin" cat > "$sandbox/bin/uname" < "$sandbox/bin/id" <<'EOF' #!/bin/sh printf '%s\n' 501 EOF cat > "$sandbox/bin/launchctl" <> '$sandbox/launchctl.log' EOF cat > "$sandbox/bin/defaults" <> '$sandbox/defaults.log' EOF chmod +x "$sandbox/bin/uname" "$sandbox/bin/id" \ "$sandbox/bin/launchctl" "$sandbox/bin/defaults" printf '%s:%s' "$sandbox/bin" "$PATH" } make_keyboard_launch_agent() { local home=$1 mkdir -p "$home/Library/LaunchAgents" touch "$home/Library/LaunchAgents/com.tarebyte.keyboard-remapping.plist" } assert_macos_preferences_applied() { local sandbox=$1 expected expected=$(printf '%s\n%s\n%s\n%s' \ "write com.apple.dock wvous-br-corner -int 13" \ "write com.apple.dock wvous-br-modifier -int 0" \ "write com.apple.screencapture location -string $sandbox/home/Pictures/Screenshots" \ "write com.apple.screencapture location-screenshot -string $sandbox/home/Pictures/Screenshots") [ "$(cat "$sandbox/defaults.log")" = "$expected" ] \ || { echo "hot corner and screenshot preferences were not configured"; exit 1; } [ -d "$sandbox/home/Pictures/Screenshots" ] \ || { echo "screenshot directory was not created"; exit 1; } } test_macos_preferences_bootstraps_agent() { local sandbox=$1 set -e local path path=$(make_macos_preferences_test_path "$sandbox" Darwin 1) make_keyboard_launch_agent "$sandbox/home" HOME="$sandbox/home" PATH="$path" "$REPO_ROOT/script/apply-macos-preferences" > /dev/null local expected expected=$(printf '%s\n%s' \ "bootstrap gui/501 $sandbox/home/Library/LaunchAgents/com.tarebyte.keyboard-remapping.plist" \ "kickstart -k gui/501/com.apple.Dock.agent") [ "$(cat "$sandbox/launchctl.log")" = "$expected" ] \ || { echo "LaunchAgent was not bootstrapped"; exit 1; } assert_macos_preferences_applied "$sandbox" } test_macos_preferences_reloads_agent() { local sandbox=$1 set -e local path path=$(make_macos_preferences_test_path "$sandbox" Darwin 0) make_keyboard_launch_agent "$sandbox/home" HOME="$sandbox/home" PATH="$path" "$REPO_ROOT/script/apply-macos-preferences" > /dev/null local expected expected=$(printf '%s\n%s\n%s' \ "bootout gui/501/com.tarebyte.keyboard-remapping" \ "bootstrap gui/501 $sandbox/home/Library/LaunchAgents/com.tarebyte.keyboard-remapping.plist" \ "kickstart -k gui/501/com.apple.Dock.agent") [ "$(cat "$sandbox/launchctl.log")" = "$expected" ] \ || { echo "loaded LaunchAgent was not reloaded"; exit 1; } assert_macos_preferences_applied "$sandbox" } test_macos_preferences_requires_agent() { local sandbox=$1 set -e local path path=$(make_macos_preferences_test_path "$sandbox" Darwin 1) if HOME="$sandbox/home" PATH="$path" \ "$REPO_ROOT/script/apply-macos-preferences" 2>/dev/null; then echo "expected failure when LaunchAgent is missing" exit 1 fi [ ! -e "$sandbox/defaults.log" ] \ || { echo "hot corner should not change without the LaunchAgent"; exit 1; } [ ! -e "$sandbox/launchctl.log" ] \ || { echo "launchctl should not run without the LaunchAgent"; exit 1; } } test_macos_preferences_preserves_screenshots() { local sandbox=$1 set -e local path path=$(make_macos_preferences_test_path "$sandbox" Darwin 0) make_keyboard_launch_agent "$sandbox/home" mkdir -p "$sandbox/home/Pictures/Screenshots" printf '%s\n' existing > "$sandbox/home/Pictures/Screenshots/example.png" HOME="$sandbox/home" PATH="$path" "$REPO_ROOT/script/apply-macos-preferences" > /dev/null HOME="$sandbox/home" PATH="$path" "$REPO_ROOT/script/apply-macos-preferences" > /dev/null [ "$(cat "$sandbox/home/Pictures/Screenshots/example.png")" = existing ] \ || { echo "existing screenshot was changed"; exit 1; } } test_macos_preferences_refreshes_screenshots() { local sandbox=$1 set -e local path pid path=$(make_macos_preferences_test_path "$sandbox" Darwin 0) make_keyboard_launch_agent "$sandbox/home" sleep 30 & pid=$! trap 'kill "$pid" 2>/dev/null || true' EXIT SYSTEM_UI_TEST_PID="$pid" HOME="$sandbox/home" PATH="$path" \ "$REPO_ROOT/script/apply-macos-preferences" > /dev/null if wait "$pid" 2>/dev/null; then echo "screenshot preference process was not restarted" exit 1 fi trap - EXIT } test_macos_preferences_skips_linux() { local sandbox=$1 set -e local path path=$(make_macos_preferences_test_path "$sandbox" Linux 1) HOME="$sandbox/home" PATH="$path" \ "$REPO_ROOT/script/apply-macos-preferences" > "$sandbox/output" [ ! -e "$sandbox/launchctl.log" ] \ || { echo "launchctl should not run on Linux"; exit 1; } [ ! -e "$sandbox/defaults.log" ] \ || { echo "defaults should not run on Linux"; exit 1; } [ ! -e "$sandbox/home/Pictures/Screenshots" ] \ || { echo "screenshot directory should not be created on Linux"; exit 1; } grep -q "not macOS; skipping preferences" "$sandbox/output" \ || { echo "missing non-macOS skip message"; exit 1; } } # ------- tool_ready ------- # # tool_ready lives in install-codespace-tools. With a version-flag probe it # must return true only when a tool is both on PATH and actually executes — # a binary present but broken (e.g. glibc mismatch, exits nonzero) must be # treated as not runnable so the installer rebuilds it. With the "none" # probe it checks presence only (tools without a version flag). extract_tool_ready_fn() { local sandbox=$1 sed -n '/^tool_ready()/,/^}/p' "$REPO_ROOT/script/install-codespace-tools" \ > "$sandbox/tool_ready_fn.sh" } test_tool_ready_working() { local sandbox=$1 set -e extract_tool_ready_fn "$sandbox" mkdir -p "$sandbox/bin" cat > "$sandbox/bin/faketool" <<'STUB' #!/bin/bash echo "faketool 1.2.3" STUB chmod +x "$sandbox/bin/faketool" # shellcheck disable=SC1091,SC2030,SC2031 ( PATH="$sandbox/bin:$PATH"; source "$sandbox/tool_ready_fn.sh"; tool_ready faketool --version ) } test_tool_ready_broken() { local sandbox=$1 set -e extract_tool_ready_fn "$sandbox" mkdir -p "$sandbox/bin" # Present on PATH but crashes at runtime, like a glibc mismatch. cat > "$sandbox/bin/faketool" <<'STUB' #!/bin/bash echo "faketool: version \`GLIBC_2.35' not found" >&2 exit 1 STUB chmod +x "$sandbox/bin/faketool" # shellcheck disable=SC1091,SC2030,SC2031 if ( PATH="$sandbox/bin:$PATH"; source "$sandbox/tool_ready_fn.sh"; tool_ready faketool --version ); then echo "a broken binary should not be reported as ready" exit 1 fi } test_tool_ready_absent() { local sandbox=$1 set -e extract_tool_ready_fn "$sandbox" # Empty PATH dir: the tool is not present at all. mkdir -p "$sandbox/bin" # shellcheck disable=SC1091,SC2030,SC2031 if ( PATH="$sandbox/bin"; source "$sandbox/tool_ready_fn.sh"; tool_ready faketool --version ); then echo "an absent tool should not be reported as ready" exit 1 fi } test_tool_ready_none_present() { local sandbox=$1 set -e extract_tool_ready_fn "$sandbox" mkdir -p "$sandbox/bin" # A tool with no version flag: "none" probe checks presence only, so a # binary that errors on --version is still considered ready. cat > "$sandbox/bin/faketool" <<'STUB' #!/bin/bash echo "usage: faketool ..." >&2 exit 1 STUB chmod +x "$sandbox/bin/faketool" # shellcheck disable=SC1091,SC2030,SC2031 ( PATH="$sandbox/bin:$PATH"; source "$sandbox/tool_ready_fn.sh"; tool_ready faketool none ) } test_tool_ready_none_absent() { local sandbox=$1 set -e extract_tool_ready_fn "$sandbox" mkdir -p "$sandbox/bin" # shellcheck disable=SC1091,SC2030,SC2031 if ( PATH="$sandbox/bin"; source "$sandbox/tool_ready_fn.sh"; tool_ready faketool none ); then echo "an absent tool should not be reported as ready (none probe)" exit 1 fi } # ------- runner ------- echo "=== stow-package ===" run_test "clean stow, no conflicts" test_stow_clean run_test "real file conflict" test_stow_real_file_conflict run_test "foreign symlink conflict" test_stow_foreign_symlink_conflict run_test "foreign directory symlink" test_stow_foreign_directory_symlink_conflict run_test "mixed conflicts and clean files" test_stow_mixed_conflicts_and_clean run_test "re-stow is idempotent" test_stow_restow_is_idempotent run_test "re-stow prunes obsolete link" test_stow_restow_prunes_obsolete_link run_test "re-stow unfolds owned directory" test_stow_restow_unfolds_owned_directory run_test "runtime state is ignored" test_stow_local_ignore_runtime_state run_test "dangling symlink conflict" test_stow_dangling_symlink_conflict run_test "missing arg errors" test_stow_missing_arg run_test "nonexistent package errors" test_stow_nonexistent_package echo echo "=== setup-git-config ===" run_test "basic template render" test_git_config_basic_render run_test "special values round trip" test_git_config_special_values run_test "codespaces render" test_git_config_codespaces_skip run_test "missing user.email errors" test_git_config_missing_email run_test "missing signing key errors" test_git_config_missing_signingkey run_test "non-SSH signing key errors" test_git_config_rejects_non_ssh_signingkey run_test "legacy identity migration" test_git_config_legacy_identity_migration run_test "prior Git identity migration" test_git_config_prior_git_identity_migration run_test "drift check" test_git_config_check_detects_drift run_test "allowed_signers render" test_git_config_allowed_signers_render run_test "allowed_signers extra appended" test_git_config_allowed_signers_extra run_test "allowed_signers skipped in cs" test_git_config_allowed_signers_codespaces run_test "allowed_signers drift check" test_git_config_check_detects_signers_drift echo echo "=== install-mise ===" run_test "installs mainline on Darwin" test_mise_installs_mainline_on_darwin run_test "skips existing mainline binary" test_mise_skips_installer_when_mainline_exists run_test "errors without installed binary" test_mise_errors_when_installer_produces_no_binary run_test "does not install on Linux" test_mise_does_not_install_on_linux echo echo "=== apply-macos-preferences ===" run_test "applies preferences and agent" test_macos_preferences_bootstraps_agent run_test "applies preferences and reloads" test_macos_preferences_reloads_agent run_test "requires stowed keyboard agent" test_macos_preferences_requires_agent run_test "preserves screenshots on reruns" test_macos_preferences_preserves_screenshots run_test "refreshes screenshot preferences" test_macos_preferences_refreshes_screenshots run_test "does not apply on Linux" test_macos_preferences_skips_linux echo echo "=== tool_ready ===" run_test "working tool ready" test_tool_ready_working run_test "broken tool (glibc) not ready" test_tool_ready_broken run_test "absent tool not ready" test_tool_ready_absent run_test "none probe: present is ready" test_tool_ready_none_present run_test "none probe: absent not ready" test_tool_ready_none_absent echo printf "${GREEN}passed: %d${NC} ${RED}failed: %d${NC}\n" "$passed" "$failed" [ "$failed" -eq 0 ]