diff --git a/.scratch/handoffs/20260721-0743-xray-proxy-05-ready-for-human.md b/.scratch/handoffs/20260721-0743-xray-proxy-05-ready-for-human.md index a3ad669..5264541 100644 --- a/.scratch/handoffs/20260721-0743-xray-proxy-05-ready-for-human.md +++ b/.scratch/handoffs/20260721-0743-xray-proxy-05-ready-for-human.md @@ -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`. diff --git a/install.sh b/install.sh index 10701f1..4b57e51 100755 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/tests/test_install.sh b/tests/test_install.sh index 7a1b56c..eb16bb7 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -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