fix(install): исправлено владение каталогом .config

Зачем:
- GNU Stow сворачивал весь ~/.config в пакет zed и помещал рабочие настройки в репозиторий.

Что:
- добавлена ограниченная миграция старой ссылки с возвратом данных в домашний каталог;
- предотвращено повторное сворачивание ~/.config на свежей установке;
- добавлены проверки сохранности профиля, повторного запуска и чужих ссылок.

Проверка:
- bash tests/test_install.sh
- bash -n install.sh tests/test_install.sh
- git diff --check
This commit is contained in:
ddadmin committed 2026-07-21 08:29:52 -04:00
1 parent fd8210db71
commit e5323284a8
3 files changed
+231 -48

No files matched your search

@@ -1,62 +1,54 @@
# Handoff: headless-xray-proxy — задача 05 готова к ручной приёмке
# Handoff: владение `~/.config` исправлено, задача 05 готова к приёмке
Дата: 2026-07-21 07:43 EDT. Заменяет прежние handoff задачи 05.
Обновлён: 2026-07-21 08:18 EDT.
## Состояние
## Состояние задачи 05
Автоматическая часть `headless-xray-proxy` зафиксирована коммитом `fd8210d`.
Тикет `.scratch/headless-xray-proxy/issues/05-package-delivery-and-acceptance.md`
переведён в `ready-for-human`. Автоматическая часть выполнена; продуктовый CLI
менять не потребовалось.
находится в `ready-for-human`; результаты тестов и команды ручной приёмки уже
записаны там. Дальше можно продолжать ручную приёмку по этому тикету.
## Что изменено
## Исправленный дефект stow
- Корневой `README.md` описывает готовый управляемый Xray и отдельное
подключение через `stow xray-proxy`.
- `xray-proxy/README.md` явно фиксирует отсутствие systemd, автозапуска и
автоматического перехода между прокси.
- `tests/test_xray_proxy_full.sh` запускает полный набор под системным Python и
CPython 3.8 через `uv`, а также Bash-тесты и проверку установки.
- Поддельный Xray в `tests/test_xray_proxy_managed.py` сохраняет отдельный
исполняемый файл и получает доступ к библиотекам исходного Python. Это
устраняет ложные падения переносимой сборки `uv`, не ломая тест удалённого
исполняемого файла.
- `tests/test_install.sh` создаёт тестовые деревья только из отслеживаемых
Git-файлов внутри одного временного каталога и удаляет его при выходе.
Корневая причина была в сворачивании GNU Stow: в пустом домашнем каталоге пакет
`zed` создавал общую ссылку `~/.config -> ~/dotfiles/zed/.config`. После этого
рабочие настройки приложений и закрытый профиль Xray физически попадали внутрь
репозитория.
## Проверка
`install.sh` теперь:
`bash tests/test_xray_proxy_full.sh` завершился с кодом 0:
- заранее создаёт настоящий каталог `~/.config`, поэтому свежая установка
создаёт только ссылку `~/.config/zed`;
- распознаёт старую ссылку лишь при точном совпадении с
`$DOTFILES_DIR/zed/.config`;
- оставляет управляемый каталог `zed` в пакете, а остальные верхнеуровневые
элементы переносит в настоящий `~/.config`;
- до первого переноса проверяет, что смог полностью перечислить данные;
- при штатной ошибке переноса возвращает уже перемещённые элементы назад;
- не раскрывает произвольные ссылки `~/.config`.
- системный Python 3.14: 79 тестов, `OK`;
- Bash-интеграция: 8 проверок, `PASS`;
- установка: `All install tests passed`, остатков `dotfiles-install-test.*` в
`/tmp` нет;
- CPython 3.8.20 через `uv`: 79 тестов, `OK`.
В `tests/test_install.sh` добавлены проверки свежей установки, миграции с
вымышленным секретом, сохранения прав `600`, отсутствия секрета в выводе,
повторного запуска и неприкосновенности произвольной ссылки.
Двухосевое ревью относительно `92cf339`:
## Проверка и живое состояние
- Standards: 3 документационных замечания исправлены, повторная проверка PASS;
- Spec: 2 документационных замечания исправлены, повторная проверка PASS;
- запахов кода и незапрошенного расширения области не найдено.
Полный набор `bash tests/test_xray_proxy_full.sh` прошёл на системном Python и
Python 3.8 через `uv`: по 79 Python-тестов, 8 проверок командной оболочки и тесты
установщика. Синтаксис сценариев командной оболочки и `git diff --check` также
прошли.
## Что осталось пользователю
Миграция применена к текущему домашнему каталогу:
Выполнить ручные команды из раздела `## Comments` тикета 05:
- `~/.config` — настоящий каталог;
- `~/.config/zed` указывает на пакет Zed;
- в `zed/.config` остался только каталог `zed`;
- рабочие каталоги приложений находятся в `~/.config`;
- `~/.config/xray-proxy/default.url` — обычный файл вне репозитория, сохранил
права `600` и одну непустую строку. Содержимое профиля не выводилось.
1. На доступной Ubuntu с Python 3.10 — установка, жизненный цикл,
`xray run -test`, HTTP и SOCKS через один локальный вход.
2. На РЕД ОС, Astra Linux или Alt Linux по доступности — тот же сценарий после
проверки версии Python.
## Текущее состояние Git
В отчёт не копировать VLESS-ссылку, UUID, ключи, `shortId`, `path` и
сгенерированный JSON. Достаточно версии системы/Python, кодов возврата и итога
HTTP/SOCKS. Недоступную систему отметить как неподтверждённую.
## После ручного результата
1. Отметить только фактически выполненные пункты тикета 05.
2. Обновить статус исходного дизайна и заявления о поддержке.
3. Обновить или удалить этот handoff.
4. По навыку `implement` выполнить итоговую проверку и conventional commit.
До ручной приёмки коммита задачи 05 нет.
Не включать в коммиты посторонние неотслеживаемые
`agents/.agents/.skill-lock.json` и `scripts/setup-xray-proxy.sh`.
+77
View File
@@ -169,6 +169,77 @@ migrate_claude_stow_symlink() {
fi
}
# Миграция: раскрыть старую stow-ссылку ~/.config -> zed/.config.
# В пакете zed остаётся только управляемый каталог zed; остальные настройки
# возвращаются в домашний каталог и больше не попадают в репозиторий.
migrate_zed_config_stow_symlink() {
local config_path="$HOME/.config"
local zed_config="$DOTFILES_DIR/zed/.config"
local config_real=""
local zed_config_real=""
local replacement=""
local items_file=""
local item=""
local base=""
local moved_items=()
[ -L "$config_path" ] || return 0
config_real="$(readlink -f "$config_path" 2>/dev/null || true)"
zed_config_real="$(readlink -f "$zed_config" 2>/dev/null || true)"
if [ -z "$zed_config_real" ] || [ "$config_real" != "$zed_config_real" ]; then
return 0
fi
info "Миграция: ~/.config больше не должен принадлежать пакету zed..."
replacement="$(mktemp -d "$HOME/.config.migrate.XXXXXX")"
chmod --reference="$zed_config_real" "$replacement"
items_file="$(mktemp "$HOME/.config.migrate-items.XXXXXX")"
if ! find "$zed_config_real" -mindepth 1 -maxdepth 1 ! -name zed -print0 > "$items_file"; then
error "Не удалось получить полный список старого ~/.config"
rm -f -- "$items_file"
rmdir "$replacement" 2>/dev/null || true
return 1
fi
while IFS= read -r -d '' item; do
base="${item##*/}"
if ! mv -- "$item" "$replacement/$base"; then
error "Не удалось перенести ~/.config/$base"
for base in "${moved_items[@]}"; do
mv -- "$replacement/$base" "$zed_config_real/$base" 2>/dev/null || true
done
rm -f -- "$items_file"
rmdir "$replacement" 2>/dev/null || true
return 1
fi
moved_items+=("$base")
done < "$items_file"
rm -f -- "$items_file"
if ! rm -- "$config_path"; then
error "Не удалось удалить старую ссылку ~/.config"
for base in "${moved_items[@]}"; do
mv -- "$replacement/$base" "$zed_config_real/$base" 2>/dev/null || true
done
rmdir "$replacement" 2>/dev/null || true
return 1
fi
if ! mv -- "$replacement" "$config_path"; then
error "Не удалось создать новый каталог ~/.config"
ln -s "$zed_config_real" "$config_path" 2>/dev/null || true
for base in "${moved_items[@]}"; do
mv -- "$replacement/$base" "$zed_config_real/$base" 2>/dev/null || true
done
rmdir "$replacement" 2>/dev/null || true
return 1
fi
info "Настройки перенесены из пакета zed в ~/.config"
}
# Настроить boot command для hproxy.local в /etc/wsl.conf.
setup_wsl_hproxy() {
[ -n "${WSL_DISTRO_NAME:-}" ] || return 0
@@ -244,6 +315,12 @@ for pkg in "${PACKAGES[@]}"; do
fi
done
migrate_zed_config_stow_symlink
if [ ! -L "$HOME/.config" ]; then
mkdir -p "$HOME/.config"
fi
if [ "${#AVAILABLE_PACKAGES[@]}" -gt 0 ]; then
stow_package "${AVAILABLE_PACKAGES[@]}"
fi
+115 -1
View File
@@ -94,7 +94,7 @@ test_fresh_install_uses_home_target() {
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 >"$fixture/install.out"
[ -L "$home_dir/.bash_custom" ] || fail "Expected .bash_custom symlink in HOME"
[ -x "$home_dir/.local/bin/xray-proxy" ] || fail "Expected executable xray-proxy CLI in HOME"
@@ -105,6 +105,116 @@ test_fresh_install_uses_home_target() {
assert_not_exists "$fixture/.bash_custom"
}
test_fresh_install_does_not_fold_config_into_zed() {
local fixture repo_root home_dir target expected
fixture="$(setup_fixture)"
repo_root="$fixture/repo"
home_dir="$fixture/home"
mkdir -p "$home_dir"
# Неполный клон: zed остаётся единственным stow-пакетом с каталогом .config.
mv "$repo_root/xray-proxy" "$fixture/xray-proxy-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"
[ -L "$home_dir/.config/zed" ] || fail "Expected ~/.config/zed symlink"
target="$(readlink -f "$home_dir/.config/zed")"
expected="$(readlink -f "$repo_root/zed/.config/zed")"
[ "$target" = "$expected" ] || fail "~/.config/zed points to $target, expected $expected"
}
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/xray-proxy"
printf '%s\n' 'btop settings' > "$legacy_config/btop/btop.conf"
printf '%s\n' 'VLESS_PROFILE_SENTINEL' > "$legacy_config/xray-proxy/default.url"
chmod 600 "$legacy_config/xray-proxy/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"
[ -L "$home_dir/.config/zed" ] || fail "Expected managed ~/.config/zed symlink"
[ -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/xray-proxy/default.url" ] || fail "Expected profile to be migrated"
[ ! -L "$home_dir/.config/xray-proxy/default.url" ] || fail "Expected profile outside the repository"
mode="$(stat -c '%a' "$home_dir/.config/xray-proxy/default.url")"
[ "$mode" = "600" ] || fail "Expected profile mode 600, got $mode"
assert_file_contains "$home_dir/.config/xray-proxy/default.url" "VLESS_PROFILE_SENTINEL"
assert_not_exists "$legacy_config/btop"
assert_not_exists "$legacy_config/xray-proxy"
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/xray-proxy/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/xray-proxy"
printf '%s\n' 'VLESS_PROFILE_SENTINEL' > "$legacy_config/xray-proxy/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/xray-proxy/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"
}
test_settings_local_is_symlinked_to_repo() {
local fixture repo_root home_dir
fixture="$(setup_fixture)"
@@ -385,6 +495,10 @@ 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_zed
test_migrates_legacy_zed_config_symlink
test_stops_legacy_migration_when_listing_fails
test_does_not_migrate_unrelated_config_symlink
test_settings_local_is_symlinked_to_repo
test_settings_local_backup_on_existing_file
test_settings_local_dir_is_skipped