#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Один корневой временный каталог на прогон: тестовые деревья копируют заметную # часть репозитория и без уборки переполняют /tmp за несколько прогонов. TEST_TMP_ROOT="$(mktemp -d /tmp/dotfiles-install-test.XXXXXX)" trap 'rm -rf "$TEST_TMP_ROOT"' EXIT fail() { echo "FAIL: $*" >&2 exit 1 } assert_file_contains() { local file="$1" local pattern="$2" grep -Fq "$pattern" "$file" || fail "Expected '$pattern' in $file" } assert_not_exists() { local path="$1" [ ! -e "$path" ] || fail "Path should not exist: $path" } setup_fixture() { local fixture_root repo_root bin_dir fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" repo_root="$fixture_root/repo" bin_dir="$fixture_root/bin" mkdir -p "$repo_root" "$bin_dir" # Список файлов соответствует свежему клону: только отслеживаемые Git-файлы # (новые файлы видны после git add). Содержимое берётся из текущего рабочего # дерева. Локальные данные инструментов внутри пакетов (vscode/.vscode-server, # zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают # stow конфликтами, поэтому в тестовое дерево не попадает. git -C "$REPO_ROOT" ls-files -z --cached --others --exclude-standard -- \ install.sh bash tmux vim zed kitty wezterm vscode agents opencode codex gemini \ claude proxy-client chatgpt scripts \ | tar -C "$REPO_ROOT" --null -T - -cf - \ | tar -xf - -C "$repo_root" cat > "$bin_dir/fake-git" <<'EOF' #!/usr/bin/env bash set -euo pipefail if [ "${1:-}" = "-C" ]; then shift 2 fi case "${1:-}" in rev-parse) exit 0 ;; show) printf '%s' "${FAKE_GIT_SHOW_CONTENT:-}" ;; *) echo "unexpected git invocation: $*" >&2 exit 1 ;; esac EOF cat > "$bin_dir/fake-sudo" <<'EOF' #!/usr/bin/env bash set -euo pipefail exec "$@" EOF chmod +x "$bin_dir/fake-git" "$bin_dir/fake-sudo" printf '%s\n' "$fixture_root" } run_install() { local repo_root="$1" local home_dir="$2" shift 2 HOME="$home_dir" \ GIT_BIN="$repo_root/../bin/fake-git" \ SUDO_BIN="$repo_root/../bin/fake-sudo" \ "$@" \ "$repo_root/install.sh" } test_fresh_install_uses_home_target() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" [ -L "$home_dir/.bash_custom" ] || fail "Expected .bash_custom symlink in HOME" [ -d "$home_dir/.local" ] || fail "Expected ~/.local directory" [ ! -L "$home_dir/.local" ] || fail "Expected ~/.local to stay a real directory" [ -d "$home_dir/.local/bin" ] || fail "Expected ~/.local/bin directory" [ ! -L "$home_dir/.local/bin" ] || fail "Expected ~/.local/bin to stay a real directory" [ -x "$home_dir/.local/bin/proxy-client" ] || fail "Expected executable proxy-client CLI in HOME" [ -x "$home_dir/.local/bin/start-chatgpt-proxy" ] || fail "Expected ChatGPT proxy launcher in HOME" [ -f "$home_dir/.local/share/applications/chatgpt-proxy.desktop" ] \ || fail "Expected ChatGPT Proxy desktop entry in HOME" [ -d "$home_dir/.local/share" ] || fail "Expected ~/.local/share directory" [ ! -L "$home_dir/.local/share" ] || fail "Expected ~/.local/share to stay a real directory" [ -d "$home_dir/.local/share/applications" ] || fail "Expected applications directory" [ ! -L "$home_dir/.local/share/applications" ] \ || fail "Expected applications directory to stay real" [ -d "$home_dir/.config/proxy-client" ] || fail "Expected proxy-client config directory" [ ! -L "$home_dir/.config/proxy-client" ] || fail "Expected proxy-client config to stay outside the repository" [ -f "$home_dir/.config/proxy-client/shell.bash" ] || fail "Expected proxy-client shell module" [ -f "$home_dir/.config/proxy-client/examples/config.external" ] || fail "Expected external proxy config example" [ -f "$home_dir/.config/proxy-client/examples/config.xray" ] || fail "Expected managed Xray config example" assert_not_exists "$home_dir/.config/proxy-client/examples/config" [ ! -e "$home_dir/README.md" ] || fail "Package README must not be stowed into HOME" [ -L "$home_dir/.claude/skills" ] || fail "Expected ~/.claude/skills symlink" assert_file_contains "$home_dir/.bashrc" "[ -f ~/.bash_custom ] && source ~/.bash_custom" assert_not_exists "$fixture/.bash_custom" } test_fresh_install_does_not_fold_config_into_kitty() { local fixture repo_root home_dir target expected fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" # Неполный клон: kitty остаётся единственным stow-пакетом с каталогом .config. mv "$repo_root/proxy-client" "$fixture/proxy-client-disabled" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" [ -d "$home_dir/.config" ] || fail "Expected ~/.config directory" [ ! -L "$home_dir/.config" ] || fail "Expected ~/.config to stay a real directory" # Каталог остаётся настоящим, симлинки ставятся по файлам: kitty пишет в # ~/.config/kitty свои файлы, и они не должны попадать в репозиторий. [ -d "$home_dir/.config/kitty" ] || fail "Expected ~/.config/kitty directory" [ ! -L "$home_dir/.config/kitty" ] || fail "Expected ~/.config/kitty to stay a real directory" [ -L "$home_dir/.config/kitty/kitty.conf" ] || fail "Expected ~/.config/kitty/kitty.conf symlink" [ -L "$home_dir/.config/kitty/current-theme.conf" ] || fail "Expected ~/.config/kitty/current-theme.conf symlink" target="$(readlink -f "$home_dir/.config/kitty/kitty.conf")" expected="$(readlink -f "$repo_root/kitty/.config/kitty/kitty.conf")" [ "$target" = "$expected" ] || fail "~/.config/kitty/kitty.conf points to $target, expected $expected" # Файл, созданный kitty рядом с управляемыми, остаётся вне репозитория. printf '%s\n' '# backup' > "$home_dir/.config/kitty/kitty.conf.bak" assert_not_exists "$repo_root/kitty/.config/kitty/kitty.conf.bak" } test_existing_kitty_config_is_backed_up_and_does_not_abort_stow() { local fixture repo_root home_dir backup fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir/.config/kitty" # kitty создаёт конфиг сам, поэтому на чужой машине файл уже существует. printf '%s\n' 'font_size 14' > "$home_dir/.config/kitty/kitty.conf" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" [ -L "$home_dir/.config/kitty/kitty.conf" ] || fail "Expected managed kitty.conf symlink" backup="$(find "$home_dir/.config/kitty" -maxdepth 1 -name 'kitty.conf.bak.*' -print -quit)" [ -n "$backup" ] || fail "Expected backup of the pre-existing kitty.conf" assert_file_contains "$backup" "font_size 14" # Конфликт в одном пакете не должен обрывать stow целиком. [ -L "$home_dir/.vimrc" ] || fail "Expected other packages to be stowed as well" [ -L "$home_dir/.bash_custom" ] || fail "Expected install to continue past stow" } test_foreign_kitty_config_symlink_is_backed_up() { local fixture repo_root home_dir backup target fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir/.config/kitty" "$fixture/elsewhere" # Чужой симлинк для stow такой же конфликт, как обычный файл. printf '%s\n' 'font_size 99' > "$fixture/elsewhere/my-kitty.conf" ln -s "$fixture/elsewhere/my-kitty.conf" "$home_dir/.config/kitty/kitty.conf" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" target="$(readlink -f "$home_dir/.config/kitty/kitty.conf")" [ "$target" = "$(readlink -f "$repo_root/kitty/.config/kitty/kitty.conf")" ] \ || fail "Expected managed kitty.conf, got $target" backup="$(find "$home_dir/.config/kitty" -maxdepth 1 -name 'kitty.conf.bak.*' -print -quit)" [ -n "$backup" ] || fail "Expected backup of the foreign kitty.conf symlink" [ -L "$home_dir/.vimrc" ] || fail "Expected other packages to be stowed as well" # Исходный файл за симлинком остаётся нетронутым. assert_file_contains "$fixture/elsewhere/my-kitty.conf" "font_size 99" } test_rerun_does_not_backup_managed_kitty_config() { local fixture repo_root home_dir backup fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install2.out" [ -L "$home_dir/.config/kitty/kitty.conf" ] || fail "Expected managed kitty.conf symlink" backup="$(find "$home_dir/.config/kitty" -maxdepth 1 -name '*.bak.*' -print -quit)" [ -z "$backup" ] || fail "Rerun must not create backups of its own symlinks: $backup" } test_existing_wezterm_config_is_backed_up() { local fixture repo_root home_dir backup fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" # Тот же конфликт, но у пакета с файлом прямо в HOME, а не в ~/.config. printf '%s\n' 'local wezterm = require("wezterm")' > "$home_dir/.wezterm.lua" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" [ -L "$home_dir/.wezterm.lua" ] || fail "Expected managed .wezterm.lua symlink" backup="$(find "$home_dir" -maxdepth 1 -name '.wezterm.lua.bak.*' -print -quit)" [ -n "$backup" ] || fail "Expected backup of the pre-existing .wezterm.lua" assert_file_contains "$backup" 'require("wezterm")' } test_folded_package_runtime_files_are_not_backed_up() { local fixture repo_root home_dir backup fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" # ~/.codex свёрнут в симлинк на пакет: файлы внутри — уже наши, не конфликт. printf '%s\n' '{}' > "$home_dir/.codex/auth.json" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install2.out" backup="$(find "$repo_root" "$home_dir" -name '*.bak.*' -print -quit)" [ -z "$backup" ] || fail "Folded package contents must not be backed up: $backup" assert_file_contains "$home_dir/.codex/auth.json" "{}" } test_migrates_legacy_zed_config_symlink() { local fixture repo_root home_dir legacy_config output mode fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" legacy_config="$repo_root/zed/.config" mkdir -p "$home_dir" "$legacy_config/btop" "$legacy_config/proxy-client" printf '%s\n' 'btop settings' > "$legacy_config/btop/btop.conf" printf '%s\n' 'VLESS_PROFILE_SENTINEL' > "$legacy_config/proxy-client/default.url" chmod 600 "$legacy_config/proxy-client/default.url" ln -s "$legacy_config" "$home_dir/.config" if ! output="$( run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never )"; then fail "Expected legacy ~/.config migration to complete" fi [ -d "$home_dir/.config" ] || fail "Expected migrated ~/.config directory" [ ! -L "$home_dir/.config" ] || fail "Expected migrated ~/.config to stop being a symlink" # zed больше не stow-пакет (см. PACKAGES), поэтому после миграции каталог # zed остаётся в репозитории и в ~/.config не появляется. assert_not_exists "$home_dir/.config/zed" [ -d "$home_dir/.config/btop" ] || fail "Expected btop config to be migrated" [ ! -L "$home_dir/.config/btop" ] || fail "Expected btop config outside the repository" [ -f "$home_dir/.config/proxy-client/default.url" ] || fail "Expected profile to be migrated" [ ! -L "$home_dir/.config/proxy-client/default.url" ] || fail "Expected profile outside the repository" mode="$(stat -c '%a' "$home_dir/.config/proxy-client/default.url")" [ "$mode" = "600" ] || fail "Expected profile mode 600, got $mode" assert_file_contains "$home_dir/.config/proxy-client/default.url" "VLESS_PROFILE_SENTINEL" assert_not_exists "$legacy_config/btop" assert_not_exists "$legacy_config/proxy-client" if printf '%s' "$output" | grep -Fq 'VLESS_PROFILE_SENTINEL'; then fail "Secret profile leaked into install output" fi run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" assert_file_contains "$home_dir/.config/proxy-client/default.url" "VLESS_PROFILE_SENTINEL" } test_stops_legacy_migration_when_listing_fails() { local fixture repo_root home_dir legacy_config target expected fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" legacy_config="$repo_root/zed/.config" mkdir -p "$home_dir" "$legacy_config/proxy-client" printf '%s\n' 'VLESS_PROFILE_SENTINEL' > "$legacy_config/proxy-client/default.url" ln -s "$legacy_config" "$home_dir/.config" cat > "$fixture/bin/find" <<'EOF' #!/usr/bin/env bash exit 7 EOF chmod +x "$fixture/bin/find" if run_install "$repo_root" "$home_dir" \ env PATH="$fixture/bin:$PATH" WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never \ >"$fixture/install.out" 2>&1; then fail "Expected migration to stop when legacy config listing fails" fi [ -L "$home_dir/.config" ] || fail "Expected legacy ~/.config symlink after listing failure" target="$(readlink -f "$home_dir/.config")" expected="$(readlink -f "$legacy_config")" [ "$target" = "$expected" ] || fail "Legacy ~/.config target changed after listing failure" assert_file_contains "$legacy_config/proxy-client/default.url" "VLESS_PROFILE_SENTINEL" } test_does_not_migrate_unrelated_config_symlink() { local fixture repo_root home_dir unrelated_config target expected fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" unrelated_config="$fixture/unrelated-config" mkdir -p "$home_dir" "$unrelated_config" printf '%s\n' 'keep me' > "$unrelated_config/local.conf" ln -s "$unrelated_config" "$home_dir/.config" # Stow вправе сообщить о конфликте, но установщик не должен раскрывать # произвольную ссылку и переносить чужие данные. run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" 2>&1 || true [ -L "$home_dir/.config" ] || fail "Expected unrelated ~/.config symlink to be preserved" target="$(readlink -f "$home_dir/.config")" expected="$(readlink -f "$unrelated_config")" [ "$target" = "$expected" ] || fail "Unrelated ~/.config symlink target changed" assert_file_contains "$unrelated_config/local.conf" "keep me" assert_not_exists "$unrelated_config/proxy-client" } test_does_not_follow_unrelated_local_symlink() { local fixture repo_root home_dir unrelated_local target expected fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" unrelated_local="$fixture/unrelated-local" mkdir -p "$home_dir" "$unrelated_local" printf '%s\n' 'keep me' > "$unrelated_local/local.conf" ln -s "$unrelated_local" "$home_dir/.local" # Установщик не должен создавать bin через произвольную ссылку ~/.local. run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" 2>&1 || true [ -L "$home_dir/.local" ] || fail "Expected unrelated ~/.local symlink to be preserved" target="$(readlink -f "$home_dir/.local")" expected="$(readlink -f "$unrelated_local")" [ "$target" = "$expected" ] || fail "Unrelated ~/.local target changed" assert_file_contains "$unrelated_local/local.conf" "keep me" assert_not_exists "$unrelated_local/bin" } test_settings_local_is_symlinked_to_repo() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to be a symlink" local target target="$(readlink -f "$home_dir/.claude/settings.local.json")" local expected expected="$(readlink -f "$repo_root/claude/.claude/settings.local.json")" [ "$target" = "$expected" ] || fail "settings.local.json points to $target, expected $expected" } test_settings_local_backup_on_existing_file() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir/.claude" echo '{"model":"sonnet"}' > "$home_dir/.claude/settings.local.json" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to become a symlink" local backup backup="$(find "$home_dir/.claude" -maxdepth 1 -name 'settings.local.json.bak.*' -print -quit)" [ -n "$backup" ] || fail "Expected backup for existing settings.local.json" assert_file_contains "$backup" '"model":"sonnet"' } test_settings_local_dir_is_skipped() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir/.claude/settings.local.json" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -d "$home_dir/.claude/settings.local.json" ] || fail "Expected directory to be preserved (not replaced)" } test_stow_migration_preserves_legacy_settings_local() { local fixture repo_root home_dir stow_target fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" # Simulate old stow layout: ~/.claude → repo dir stow_target="$repo_root/claude/.claude" ln -s "$stow_target" "$home_dir/.claude" # Put a legacy settings.local.json in the stow target (would have been gitignored before) echo '{"model":"opus"}' > "$stow_target/legacy-local.json" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -d "$home_dir/.claude" ] || fail "Expected ~/.claude to become a directory" [ ! -L "$home_dir/.claude" ] || fail "Expected ~/.claude to stop being a symlink" [ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json symlink after migration" # legacy-local.json should have been moved from stow dir to real ~/.claude [ -f "$home_dir/.claude/legacy-local.json" ] || fail "Expected legacy file to be migrated" } test_rerun_does_not_duplicate_bashrc_line() { local fixture repo_root home_dir count fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out count="$(grep -Fxc '[ -f ~/.bash_custom ] && source ~/.bash_custom' "$home_dir/.bashrc")" [ "$count" -eq 1 ] || fail "Expected bash_custom line once, got $count" } test_repairs_nested_skills_directory_bug() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir/.claude/skills" ln -s "$repo_root/agents/.agents/skills" "$home_dir/.claude/skills/skills" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -L "$home_dir/.claude/skills" ] || fail "Expected repaired ~/.claude/skills symlink" assert_not_exists "$home_dir/.claude/skills/skills" } test_preserves_nonempty_skills_directory() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir/.claude/skills" echo local > "$home_dir/.claude/skills/local.txt" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -d "$home_dir/.claude/skills" ] || fail "Expected ~/.claude/skills directory to be preserved" [ -f "$home_dir/.claude/skills/local.txt" ] || fail "Expected local skill data to be preserved" } test_opencode_runtime_dir_is_preserved_and_config_is_managed() { local fixture repo_root home_dir backup_path fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir/.config/opencode" echo runtime > "$home_dir/.config/opencode/package.json" cat > "$home_dir/.config/opencode/opencode.json" <<'EOF' {"source":"local"} EOF run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -f "$home_dir/.config/opencode/package.json" ] || fail "Expected opencode runtime files to be preserved" [ -L "$home_dir/.config/opencode/agents" ] || fail "Expected ~/.config/opencode/agents symlink" [ -L "$home_dir/.config/opencode/skills" ] || fail "Expected ~/.config/opencode/skills symlink" assert_file_contains "$home_dir/.config/opencode/opencode.json" '"$schema": "https://opencode.ai/config.json"' backup_path="$(find "$home_dir/.config/opencode" -maxdepth 1 -name 'opencode.json.bak.*' -print -quit)" [ -n "$backup_path" ] || fail "Expected backup for existing opencode.json" assert_file_contains "$backup_path" '"source":"local"' } test_broken_claude_symlink_does_not_fail() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" ln -s "$fixture/missing-claude-dir" "$home_dir/.claude" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -d "$home_dir/.claude" ] || fail "Expected ~/.claude directory after migration" [ ! -L "$home_dir/.claude" ] || fail "Expected ~/.claude to stop being a symlink" } test_absolute_symlinks_in_home_do_not_break_install() { local fixture repo_root home_dir fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" mkdir -p "$home_dir" "$fixture/windows/.aws" "$fixture/windows/.azure" ln -s "$fixture/windows/.aws" "$home_dir/.aws" ln -s "$fixture/windows/.azure" "$home_dir/.azure" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -L "$home_dir/.aws" ] || fail "Expected ~/.aws symlink to survive install" [ -L "$home_dir/.azure" ] || fail "Expected ~/.azure symlink to survive install" } test_wsl_noninteractive_skips_file_changes() { local fixture repo_root home_dir wsl_conf output fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" wsl_conf="$fixture/wsl.conf" mkdir -p "$home_dir" printf '[automount]\n' > "$wsl_conf" output="$( run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME=Ubuntu INSTALL_INTERACTIVE_MODE=never WSL_CONF_PATH="$wsl_conf" )" assert_file_contains "$wsl_conf" "[automount]" if grep -Fq '[boot]' "$wsl_conf"; then fail "WSL config should not be modified in non-interactive mode" fi printf '%s' "$output" | grep -Fq 'Добавьте вручную:' || fail "Expected manual WSL hint" } test_wsl_noninteractive_existing_boot_command_is_preserved() { local fixture repo_root home_dir wsl_conf output fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" wsl_conf="$fixture/wsl.conf" mkdir -p "$home_dir" cat > "$wsl_conf" <<'EOF' [boot] command = "/usr/local/bin/existing.sh" EOF output="$( run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME=Ubuntu INSTALL_INTERACTIVE_MODE=never WSL_CONF_PATH="$wsl_conf" )" assert_file_contains "$wsl_conf" 'command = "/usr/local/bin/existing.sh"' if grep -Fq 'setup-hproxy.sh' "$wsl_conf"; then fail "Expected existing boot command to stay untouched" fi printf '%s' "$output" | grep -Fq 'уже содержит [boot] command' || fail "Expected warning about existing boot command" if printf '%s' "$output" | grep -Fq 'Добавьте вручную:'; then fail "Did not expect manual add hint when [boot] command already exists" fi } test_wsl_legacy_boot_command_suggests_migration() { local fixture repo_root home_dir wsl_conf output command_line fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" wsl_conf="$fixture/wsl.conf" mkdir -p "$home_dir" cat > "$wsl_conf" <<'EOF' [boot] command = "ip route show default | awk '{print $3 \" hproxy.local\"}' >> /etc/hosts" EOF command_line="command = \"$repo_root/scripts/setup-hproxy.sh\"" output="$( run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME=Ubuntu INSTALL_INTERACTIVE_MODE=never WSL_CONF_PATH="$wsl_conf" )" assert_file_contains "$wsl_conf" 'command = "ip route show default | awk '\''{print $3 \" hproxy.local\"}'\'' >> /etc/hosts"' printf '%s' "$output" | grep -Fq 'legacy boot command' || fail "Expected legacy boot command warning" printf '%s' "$output" | grep -Fq 'Рекомендуется заменить' || fail "Expected migration hint to replace legacy command" printf '%s' "$output" | grep -Fq "$command_line" || fail "Expected suggested setup-hproxy.sh command" } test_wsl_interactive_adds_boot_command() { local fixture repo_root home_dir wsl_conf command_line fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" wsl_conf="$fixture/wsl.conf" mkdir -p "$home_dir" printf '[automount]\n' > "$wsl_conf" command_line="command = \"$repo_root/scripts/setup-hproxy.sh\"" printf 'y\n' | run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME=Ubuntu INSTALL_INTERACTIVE_MODE=always WSL_CONF_PATH="$wsl_conf" >/tmp/test-install.out assert_file_contains "$wsl_conf" "[boot]" assert_file_contains "$wsl_conf" "$command_line" } test_wsl_existing_boot_command_is_preserved() { local fixture repo_root home_dir wsl_conf output fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" wsl_conf="$fixture/wsl.conf" mkdir -p "$home_dir" cat > "$wsl_conf" <<'EOF' [boot] command = "/usr/local/bin/existing.sh" EOF output="$( printf 'y\n' | run_install "$repo_root" "$home_dir" \ env WSL_DISTRO_NAME=Ubuntu INSTALL_INTERACTIVE_MODE=always WSL_CONF_PATH="$wsl_conf" )" assert_file_contains "$wsl_conf" 'command = "/usr/local/bin/existing.sh"' if grep -Fq 'setup-hproxy.sh' "$wsl_conf"; then fail "Expected existing boot command to stay untouched" fi printf '%s' "$output" | grep -Fq 'уже содержит [boot] command' || fail "Expected warning about existing boot command" } main() { command -v stow >/dev/null 2>&1 || fail "stow is required for tests" test_fresh_install_uses_home_target test_fresh_install_does_not_fold_config_into_kitty test_existing_kitty_config_is_backed_up_and_does_not_abort_stow test_foreign_kitty_config_symlink_is_backed_up test_rerun_does_not_backup_managed_kitty_config test_existing_wezterm_config_is_backed_up test_folded_package_runtime_files_are_not_backed_up test_migrates_legacy_zed_config_symlink test_stops_legacy_migration_when_listing_fails test_does_not_migrate_unrelated_config_symlink test_does_not_follow_unrelated_local_symlink test_settings_local_is_symlinked_to_repo test_settings_local_backup_on_existing_file test_settings_local_dir_is_skipped test_stow_migration_preserves_legacy_settings_local test_rerun_does_not_duplicate_bashrc_line test_repairs_nested_skills_directory_bug test_preserves_nonempty_skills_directory test_opencode_runtime_dir_is_preserved_and_config_is_managed test_broken_claude_symlink_does_not_fail test_absolute_symlinks_in_home_do_not_break_install test_wsl_noninteractive_skips_file_changes test_wsl_noninteractive_existing_boot_command_is_preserved test_wsl_legacy_boot_command_suggests_migration test_wsl_interactive_adds_boot_command test_wsl_existing_boot_command_is_preserved echo "All install tests passed" } main "$@"