fix(install): исправлена установка dotfiles и добавлены тесты
- Зачем: - нужно было убрать баги install.sh и сделать регрессии воспроизводимыми. - Что: - исправлена установка через stow в HOME, миграция ~/.claude и копирование settings.json без разрушения рабочей директории. - стабилизирована WSL-логика через проверки интерактивности и защиту от дублирования boot command. - добавлен tests/test_install.sh с regression-сценариями для install.sh. - Проверка: - bash -n install.sh - bash -n tests/test_install.sh - ./tests/test_install.sh
This commit is contained in:
1 parent
e00611015a
commit
edeb8da3c2
2 files changed
+375
-35
No files matched your search
Executable
+257
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
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 /tmp/dotfiles-install-test.XXXXXX)"
|
||||
repo_root="$fixture_root/repo"
|
||||
bin_dir="$fixture_root/bin"
|
||||
|
||||
mkdir -p "$repo_root" "$bin_dir"
|
||||
cp -a \
|
||||
"$REPO_ROOT/install.sh" \
|
||||
"$REPO_ROOT/bash" \
|
||||
"$REPO_ROOT/tmux" \
|
||||
"$REPO_ROOT/vim" \
|
||||
"$REPO_ROOT/zed" \
|
||||
"$REPO_ROOT/vscode" \
|
||||
"$REPO_ROOT/agents" \
|
||||
"$REPO_ROOT/opencode" \
|
||||
"$REPO_ROOT/codex" \
|
||||
"$REPO_ROOT/gemini" \
|
||||
"$REPO_ROOT/claude" \
|
||||
"$REPO_ROOT/scripts" \
|
||||
"$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 >/tmp/test-install.out
|
||||
|
||||
[ -L "$home_dir/.bash_custom" ] || fail "Expected .bash_custom symlink in 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_dirty_repo_settings_copy_uses_committed_version() {
|
||||
local fixture repo_root home_dir
|
||||
fixture="$(setup_fixture)"
|
||||
repo_root="$fixture/repo"
|
||||
home_dir="$fixture/home"
|
||||
mkdir -p "$home_dir"
|
||||
|
||||
chmod u+w "$repo_root/claude/.claude/settings.json"
|
||||
cat > "$repo_root/claude/.claude/settings.json" <<'EOF'
|
||||
{"source":"working-tree"}
|
||||
EOF
|
||||
|
||||
FAKE_GIT_SHOW_CONTENT='{"source":"head"}' \
|
||||
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
|
||||
|
||||
assert_file_contains "$home_dir/.claude/settings.json" '"source":"head"'
|
||||
assert_file_contains "$repo_root/claude/.claude/settings.json" '"source":"working-tree"'
|
||||
}
|
||||
|
||||
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_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_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_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_dirty_repo_settings_copy_uses_committed_version
|
||||
test_rerun_does_not_duplicate_bashrc_line
|
||||
test_repairs_nested_skills_directory_bug
|
||||
test_preserves_nonempty_skills_directory
|
||||
test_broken_claude_symlink_does_not_fail
|
||||
test_wsl_noninteractive_skips_file_changes
|
||||
test_wsl_interactive_adds_boot_command
|
||||
test_wsl_existing_boot_command_is_preserved
|
||||
|
||||
echo "All install tests passed"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in new issue
Block a user