Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1. Build: - версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и замораживается в metadata пакета (version + immutable url + sha256); - compatibility gate: реальный бинарник должен принять канонический конфиг HY2XS для gecko и salamander до создания пакета; - сборка прогоняет тесты оркестратора и админки. Конфигурационный контракт: - HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast; - obfs стал настоящим union gecko|salamander, gecko — default; - obfs-блок рендерится оркестратором целиком, два подтипа одновременно структурно невозможны; - современный baseline: congestion bbr/standard, disableLossCompensation=false, disableStatelessReset=false, полный quic-блок. Исправления: - share URI для gecko: генератор был завязан на Obfs.Salamander.Password и выдавал нерабочую ссылку при любой другой обфускации; - SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file; - экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль; - экспорт терял неизвестные upstream-поля при round-trip через типизированную модель; - renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом; - namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0). Тесты: - 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover; - тесты URI и экспорта в Go; - tools/test/e2e-hysteria.sh с реальным клиентом Hysteria. UX: - подсказки и примеры в форме создания пира. Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов), документация на русском.
18 KiB
Testing and acceptance
Цель документа
Зафиксировать checklist для новой двухслойной схемы.
Как запускать тесты
# Юнит-тесты и типы оркестратора
cd orchestrator && bun install --frozen-lockfile && bun run check && bun test
# Тесты и статический анализ HY2XS admin
cd apps && go vet ./... && go test ./...
# Полный E2E с реальным клиентом Hysteria (Debian 13 amd64)
HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
# Production-сборка: прогоняет тесты, резолвер и compatibility gate
./tools/build/build.sh
build.sh останавливается, если падают тесты оркестратора, тесты админки или compatibility gate.
A. Builder layer tests
Проверяем
- builder запускается на Debian 13 amd64 build host
- итоговый пакет собирается без target-side шагов
- bundled HY2XS admin реально входит в пакет
- package metadata / build id присутствуют
- compiled Bun/TypeScript orchestrator artifact присутствует
- в пакет не попадает build-мусор
- builder сам доставляет отсутствующие build-зависимости
- builder проверяет версии Go/Bun/Node.js/pnpm
- builder пишет версии toolchain в metadata
- builder прогоняет
bun testиgo testдо упаковки
A1. Latest-stable resolver
Фикстуры и ожидаемое поведение (orchestrator/test/hysteria-release.test.ts):
| Сценарий | Ожидание |
|---|---|
stable app/v2.12.2 |
выбирается |
prerelease app/v2.13.0 |
игнорируется |
draft app/v2.14.0 |
игнорируется |
чужое семейство тегов (core/, docs/) |
игнорируется |
тег без префикса app/ |
игнорируется |
app/v2.9.10 против app/v2.9.2 |
выбирается 2.9.10 (числовое сравнение, не строковое) |
отсутствует hysteria-linux-amd64 |
ошибка |
дублирующийся hysteria-linux-amd64 |
ошибка, а не случайный выбор |
| non-https URL артефакта | ошибка |
| невалидный semver в теге | игнорируется |
| пустой список релизов | понятная ошибка |
| несовпадение SHA-256 | сборка падает |
| сетевая ошибка / rate limit | понятная ошибка с подсказкой про GITHUB_TOKEN и HYSTERIA_CHANNEL=pinned |
Отдельно проверяется, что latest stable — это именно stable, а не максимальная строка или самый свежий тег.
A2. Release rollover
Ключевой acceptance-критерий модели «latest на сборке»:
Сегодня: latest = 2.12.2 → пакет A закрепляет 2.12.2
Завтра: latest = 2.12.3 → пакет B закрепляет 2.12.3
Повторная установка пакета A всё равно ставит 2.12.2
Проверяется на двух уровнях:
- резолвер даёт разный результат на разных снимках upstream (
orchestrator/test/release-rollover.test.ts); - install-time код не импортирует резолвер, не обращается к
api.github.comи не использует movinglatest— это утверждение проверяется тестом и acceptance-шагом сборки.
A3. Compatibility gate
- скачанный артефакт проходит проверку SHA-256;
hysteria versionсовпадает с разрешённой версией;- реальный бинарник принимает канонический конфиг HY2XS для Gecko;
- то же для Salamander;
- при несовместимости падает сборка с сообщением
BUILD FAILED: unsupported Hysteria stable vX.Y.Z, а не установка у пользователя.
A4. Конфигурационный контракт (unit)
Таблица orchestrator/test/env.test.ts:
| Вход | Ожидание |
|---|---|
| значение не задано | gecko |
gecko |
принято |
salamander |
принято |
| неизвестный тип | отклонено |
Gecko (регистр) |
отклонено |
gecko max < min |
отклонено |
gecko max > 2048 |
отклонено |
gecko max == 2048 |
принято |
неположительный/нецелый min |
отклонено |
| пустой obfs-пароль | автогенерация, а не пустое значение в конфиге |
HY2XS_CONFIG_SCHEMA_VERSION=1 |
отклонено с указанием на чистую установку |
Отдельно — round-trip parse(render(config)) == config. Этот тест ловит класс ошибок «в рендер runtime-конфига попал литерал вместо значения из конфигурации».
Рендер конфига (orchestrator/test/render-config.test.ts):
- Gecko рендерит только gecko-подблок;
- Salamander рендерит только salamander-подблок;
- в конфиге никогда нет двух подтипов obfs одновременно;
- шаблон не содержит захардкоженного типа обфускации;
- пароль с пробелами и спецсимволами экранируется;
- YAML-инъекция через пароль отклоняется даже в обход env-валидации.
B. Target install tests
На чистом Debian 13 проверяем
- пакет запускается без ручной сборки на сервере
- Hysteria2 скачивается с official upstream
- bundled HY2XS admin раскладывается локально из пакета
- создаются нужные каталоги
- создаются systemd unit-файлы
- создаются
hy2xs.envиpost-install.envс правами0600 root:root - baseline firewall применяется корректно через staged mode
- SSH остаётся доступным
reconfigure --dry-runвыводит план измененийreconfigure --applyприменяет изменения и проходит smoke
C. Runtime tests
hysteria-serveractivehy2xs-adminactive- Hysteria слушает только IPv4 (
0.0.0.0:<udp_port>) - HY2XS admin слушает ожидаемый
HY2XS_UI_BIND_HOST:<ui_port> - тестовый совместимый клиент подключается
- идёт реальный трафик
- лимит 50/50 Mbps соблюдается при согласованной клиентской конфигурации
- reboot не ломает baseline
- Hysteria2 управляется systemd unit, а не внутренним updater'ом admin panel
- нет IPv6 listen (
[::]) для Hysteria/HY2XS admin trafficStats.secretне равенJWT_SECRET- bootstrap admin secret существует и имеет
0600 trafficStatsAPI: корректный secret принимает запрос, неверный secret отклоняется- TLS mode в
config.yamlсоответствует runtime env (acme|file|self_signed_dev) - при
HY2XS_TLS_MODE=acmeвconfig.yamlвыставленacme.typeизHY2XS_ACME_TYPE - direct
hysteria2://node URL в API/QR формируется поHY2XS_PUBLIC_HOST+HY2XS_PUBLIC_PORT; subscription delivery endpoint отключён в baseline и не входит в acceptance nft -c -f /etc/nftables.confпроходит после apply- пароль admin и
con_passне перезаписываются при рестартеhy2xs-admin - остановка/рестарт UI не останавливает
hysteria-server - traffic accounting/kick ориентируются на systemd status, а не на SQLite
HYSTERIA2_ENABLE /etc/hysteria/config.yamlимеет0640 hysteria:hy2xs-adminhy2xs-adminможет читать/etc/hysteria/config.yaml, но не может писать
C1. Семантический smoke конфига
Недостаточно grep по YAML: он не отличит нужное поле от такой же строки в другой секции и не заметит оставшийся рядом лишний подблок.
Smoke разбирает /etc/hysteria/config.yaml и сверяет с production-профилем:
effective Hysteria version == версия из metadata пакета
obfs:
type == HY2XS_HYSTERIA_OBFS_TYPE
ровно один подблок, соответствующий type
password непустой
для gecko: minPacketSize == 512, maxPacketSize == 1200
bandwidth:
up/down == runtime env
disableLossCompensation == false
congestion:
type == bbr
bbrProfile == standard
quic:
disableStatelessReset == false
окна и таймауты == baseline
trafficStats:
listen == runtime env
secret непустой
auth:
type == http
url содержит machine access_token
TLS:
acme-режим не содержит секции tls
file-режим не содержит секции acme
C2. End-to-end с реальным клиентом
tools/test/e2e-hysteria.sh, отдельно для Gecko и Salamander:
- сервер принимает сгенерированный конфиг и стартует;
- TLS handshake;
- handshake с обфускацией;
- HTTP auth HY2XS: разрешённый пир принят;
- HTTP auth HY2XS: неразрешённый пир отклонён;
- клиент подключается именно по сгенерированной
hysteria2://ссылке; - TCP forwarding;
- UDP forwarding;
trafficStatsс валидным secret;trafficStatsс невалидным secret отклоняется;- per-peer accounting содержит аутентифицированного пира;
- перезапуск сервера;
- быстрое переподключение клиента (поведение stateless reset).
Пункт 6 — тот самый, который ловит класс ошибок, неизбежный при наивном включении Gecko: сервер работает, ссылка формально валидна, а клиент по ней не подключается.
C3. Share URI (unit)
apps/service/hysteria2_api_test.go:
- Gecko URI содержит
obfs=geckoиobfs-password; - Salamander URI содержит
obfs=salamanderиobfs-password; - конфиг без обфускации даёт ссылку без
obfs; - неизвестный тип обфускации в ссылку не попадает;
- обфускация без пароля в ссылку не попадает;
- SNI: ACME-домен →
HY2XS_DOMAIN→HY2XS_PUBLIC_HOST, IP не используется; - спецсимволы в credentials и obfs-пароле переживают round-trip:
+, пробел,#,@,/,?,&,=,%, кириллица; - литеральный
+кодируется как%2Bи не схлопывается с пробелом (регрессия на upstream-баг 2.9.3).
C4. Экспорт конфига (unit)
apps/service/hysteria2_export_test.go:
- неизвестные upstream-секции переживают экспорт целиком, включая вложенные карты и списки;
- операционные поля остаются читаемыми;
- вырезаются: obfs-пароль,
trafficStats.secret,access_token,auth.userpass, учётные данные ACME DNS, пароли outbound; - вырезается неизвестное поле с секретным именем;
- пути к файлам (
tls.key,ech.keyPath,clientCA) остаются видимыми.
D. Negative tests
- не Debian 13
- порт уже занят
- старое конфликтующее состояние уже существует
- домен / SNI заданы некорректно
- bundled UI отсутствует в пакете
- Hysteria upstream недоступен
- firewall применился частично
- install flow прерван посередине
- попытка использовать
HY2XS_IPV6_ENABLED=true HY2XS_PUBLIC_HOST=0.0.0.0- неизвестный
HY2XS_HYSTERIA_OBFS_TYPE - конфигурация со схемой
HY2XS_CONFIG_SCHEMA_VERSIONиз линейки0.x - upstream
latestнесовместим с шаблоном HY2XS — падает сборка, не установка
E. Fix20 production matrix (обязательные сценарии)
-
Clean Debian 13 minimal:
- только SSH, без ручной установки зависимостей;
- default
/etc/nftables.confstub; - install проходит полностью;
doctor/statusпоказывают рабочее состояние.
-
Non-systemd container:
- fail-fast до destructive шагов;
- диагностическое сообщение с причиной capability/systemd.
-
Foreign nftables:
- при
HY2XS_FIREWALL_MODE=managedinstall/reconfigure блокируются; - при
HY2XS_FIREWALL_MODE=takeoverсоздаются backup/rollback guard и apply проходит.
- при
-
Rollback guard cleanup:
- после успешного apply/smoke не остаются
hy2xs-fw-rollback-*.timer/.service.
- после успешного apply/smoke не остаются
-
Partial install + repair:
- состояние
install-stateфиксирует промежуточную фазу; repairзавершает граф доinstalled=true.
- состояние
-
AAAA при IPv4-only:
- policy строго валидируется preflight;
- soft warning path не используется в production baseline.
-
Slow-start admin readiness:
- install не падает на race после restart;
- readiness waiters дожидаются listener/healthz.
Acceptance criteria
Система принимается, если:
- production builder на Debian 13 amd64 выдаёт переносимый install package
- target server не выполняет build step
- Hysteria2 получена из official upstream
- HY2XS admin поставлен из install package
post-install.envотражает фактическое deploy-состояние- оркестратор зафиксирован как Bun/TypeScript stack и поставляется как готовый install-артефакт
- оркестратор не требует standalone update / rollback / uninstall subcommands
- bounded rollback в install/reconfigure корректно отрабатывает failure-сценарии firewall/systemd/config/smoke
- Telegram/access layer не требуется для прохождения install acceptance
- отсутствует production path для port hopping
- UI не запускается от root
- клиентские endpoint не зависят от request
Host/hostname - production build verify падает, если
config/hy2xs.envсодержит placeholder-значения - production build verify падает при dirty git tree (кроме
ALLOW_DIRTY_BUILD=true) - metadata содержит
source_git_commit,dirty_tree,build_profile=production - builder без override на сегодняшний день автоматически выбирает последнюю стабильную версию Hysteria
- собранный пакет содержит точные версию, URL и SHA-256
- выход новой версии Hysteria после сборки не меняет содержимое старого пакета
- новая установка генерирует Gecko
- Gecko использует
512/1200 - установленная Hysteria реально принимает сгенерированный YAML
- сервис запускается под существующим непривилегированным пользователем
hysteria - созданный пользователь получает
hysteria2://сobfs=geckoиobfs-password - совместимый клиент Hysteria подключается напрямую по этой ссылке
- после перезапуска Hysteria клиент быстро восстанавливает соединение
- режим
HY2XS_HYSTERIA_OBFS_TYPE=salamanderполностью работоспособен - admin читает Gecko-конфиг без ошибок
- экспорт не уничтожает современные и неизвестные upstream-поля
- экспорт не содержит секретов
- frontend отображает Gecko
namedotcomудалён, актуальные ACME-провайдеры отражены- документация нигде не утверждает, что Salamander — фиксированный инвариант
- документация не фиксирует конкретный номер версии как «текущую версию», а объясняет latest-stable build policy
- форма создания пира содержит примеры значений и пояснения для полей «Пир», «Комментарий» и «Секрет»