b99be7d514
Сборка не собиралась: два контракта приёмки роняли её на корректном коде.
verify_api_namespace_contract искал возвращение legacy-пространства имён
через grep по '/hui' и находил router_test.go, который ПЕРЕЧИСЛЯЕТ этот
префикс, чтобы доказать отсутствие маршрута, и сам versions.sh, где строка
стоит в тексте проверки. Падение приходило шестым шагом из четырнадцати, до
резолва Hysteria. За ним прятался второй такой же: проверка транзакционности
импорта пиров брала файл от начала applyPeerImportEntry и до конца, захватывая
объявленные ниже ExistPeerName и UpdatePeerLastConnectionAt.
Обе проверки теперь смотрят на код, а не на упоминания: добавлены помощники
code_without_comments и code_mentions_in, а отсутствие legacy-маршрута
доказывает тест на таблице маршрутов собранного роутера.
Планировщик стал собственностью процесса. InitCron вызывался из runServer и
на каждом вызове создавал новый cron.New(), не сохраняя ссылку; cron.Stop()
не вызывался нигде. Смена RESET_TRAFFIC_CRON выполняла StopServer(), точка
входа крутила for { runServer() } — и каждая правка добавляла целый
дублирующий набор джоб, а старое расписание сброса продолжало работать.
Фиксированные джобы регистрируются один раз, расписание переносится на месте
по EntryID, HTTP-сервер не трогается. Добавлено штатное завершение по SIGTERM.
Выражение проверяется до записи в базу тем же парсером (cron.ParseStandard),
которым его разбирает планировщик: раньше невалидная строка сохранялась, API
отвечал успехом, а сброс трафика молча исчезал.
updateConfigs стал атомарным: полная проверка партии, одна транзакция,
применение к рантайму. Прежний тест ставил запрещённый ключ первым и не
смотрел в базу — поймать частичное применение он был неспособен.
Удалены четыре ключа таблицы config без единого потребителя: HYSTERIA2_ENABLE,
HYSTERIA2_CONFIG (второй источник истины, читался первым), HYSTERIA2_TRAFFIC_TIME
и HYSTERIA2_CONFIG_REMARK. Имя профиля в share URI выводится из имени пира.
Безопасность:
- bootstrap-пароль администратора больше не генерируется и не пишется в журнал,
который отдаётся кнопкой выгрузки; отсутствие env — отказ старта;
- собственный журнал админки санитизируется наравне с чужим;
- golang-jwt/jwt v3 -> v5: GO-2025-3553 не имеет исправленной версии в v3 и
достижима с неаутентифицированного запроса; набор алгоритмов подписи
зафиксирован через WithValidMethods;
- удалён вход по несолёному SHA-224 из предыдущего поколения;
- убран modulo bias в util.RandomString — единственном генераторе секретов;
- пир установщика защищён во всех путях записи, а не только в импорте;
- удалена латентная паника в service.GetToken и недостижимая ветка GetAdminInfo,
проверявшая меньше, чем middleware.
Toolchain: Go 1.21.13 -> 1.26.7, Node 20.19.0 (EOL) -> 24.20.0. На прежнем
графе govulncheck находил 21 вызываемую уязвимость, 17 из них в stdlib,
попадающей в production-бинарь. Сейчас — ноль. Добавлен обязательный шаг
проверки зависимостей (govulncheck + pnpm audit) с записью результата в
metadata пакета.
814 lines
55 KiB
Markdown
814 lines
55 KiB
Markdown
# Testing and acceptance
|
||
|
||
## Цель документа
|
||
|
||
Зафиксировать checklist для новой двухслойной схемы.
|
||
|
||
## Как запускать тесты
|
||
|
||
```bash
|
||
# Юнит-тесты и типы оркестратора
|
||
cd orchestrator && bun install --frozen-lockfile && bun run check && bun test
|
||
|
||
# Тесты и статический анализ HY2XS admin
|
||
cd apps && go vet ./... && go test ./...
|
||
|
||
# Полный E2E с реальным клиентом Hysteria (Debian 13 amd64; нужен Go)
|
||
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
|
||
|
||
### Проверяем
|
||
1. builder запускается на Debian 13 amd64 build host
|
||
2. итоговый пакет собирается без target-side шагов
|
||
3. bundled HY2XS admin реально входит в пакет
|
||
4. package metadata / build id присутствуют
|
||
5. compiled Bun/TypeScript orchestrator artifact присутствует
|
||
6. в пакет не попадает build-мусор
|
||
7. builder сам доставляет отсутствующие build-зависимости
|
||
8. builder проверяет версии Go/Bun/Node.js/pnpm
|
||
9. builder пишет версии toolchain в metadata
|
||
10. 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 на сборке»:
|
||
|
||
```text
|
||
Сегодня: 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` и не использует moving `latest` — это утверждение проверяется тестом и acceptance-шагом сборки.
|
||
|
||
## A3. Compatibility gate
|
||
|
||
1. скачанный артефакт проходит проверку SHA-256;
|
||
2. `hysteria version` совпадает с разрешённой версией;
|
||
3. реальный бинарник принимает канонический конфиг HY2XS для Gecko;
|
||
4. то же для Salamander;
|
||
5. при несовместимости падает **сборка** с сообщением `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` | отклонено с указанием на чистую установку |
|
||
| `HY2XS_CONFIG_SCHEMA_VERSION` отсутствует | отклонено как legacy-конфигурация |
|
||
| `HY2XS_CONFIG_SCHEMA_VERSION=` (пусто) | отклонено как legacy-конфигурация |
|
||
| `HY2XS_CONFIG_SCHEMA_VERSION=2` | принято |
|
||
|
||
Отсутствие маркера схемы отклоняется намеренно: до v1 этого поля не
|
||
существовало, поэтому именно пустое значение — самый вероятный признак
|
||
конфигурации 0.x. Любой fallback здесь молча превращал бы legacy-конфиг в
|
||
якобы валидный.
|
||
|
||
Отдельно — round-trip `parse(render(config)) == config`. Этот тест ловит класс ошибок «в рендер runtime-конфига попал литерал вместо значения из конфигурации».
|
||
|
||
Рендер конфига (`orchestrator/test/render-config.test.ts`):
|
||
|
||
- Gecko рендерит **только** gecko-подблок;
|
||
- Salamander рендерит **только** salamander-подблок;
|
||
- в конфиге никогда нет двух подтипов obfs одновременно;
|
||
- шаблон не содержит захардкоженного типа обфускации;
|
||
- пароль с пробелами и спецсимволами экранируется;
|
||
- YAML-инъекция через пароль отклоняется даже в обход env-валидации.
|
||
|
||
## A5. Граница установки и поколение (unit)
|
||
|
||
`orchestrator/test/clean-host.test.ts`:
|
||
|
||
- чистый хост проходит;
|
||
- **каждый** маркер по отдельности останавливает установку;
|
||
- список покрывает состояние, юниты, бинарник Hysteria и наследие 0.x;
|
||
- пути из конфигурации (`HY2XS_INSTALL_DIR`, `HY2XS_DATA_DIR`, `HY2XS_LOG_DIR`)
|
||
попадают в список, а не только значения по умолчанию;
|
||
- всё, что удаляет `purge-v0.sh`, покрыто маркерами clean-host: два списка
|
||
описывают одну границу и не имеют права разъезжаться;
|
||
- bootstrap-пути (`/usr/local/lib/hy2xs`, `/usr/local/lib/hy2xs/package`,
|
||
`/usr/local/bin/hy2xs-orchestrator`) остаются маркерами **без исключений**:
|
||
их создаёт оркестратор уже после проверки чистоты хоста, поэтому «мягкой»
|
||
версии списка для PHASE 1 больше не существует;
|
||
- эти пути берутся из `config/profile.ts`, а не из копий строк: шаг, который
|
||
их создаёт, и контракт, который на них отказывает, обязаны читать одно
|
||
значение;
|
||
- сообщение перечисляет найденные маркеры и говорит, что хост не изменён.
|
||
|
||
`orchestrator/test/install-boundary.test.ts`:
|
||
|
||
- под read-only guard недоступны `writeText`, `writeTextAtomic` и все
|
||
`runMutating*`-раннеры;
|
||
- read-only раннеры под guard'ом продолжают работать: разделение API — это не
|
||
запрет наблюдения, а запрет мутации;
|
||
- классификация отказа зависит от ownership-флагов и фазы, а **не** от текста
|
||
ошибки;
|
||
- `fatal_pre_apply` недостижим ни при одном взведённом флаге, включая
|
||
`stateWritten`: записанный `install-state.json` уже делает хост изменённым;
|
||
- начатая (не обязательно завершённая) установка пакетов уже даёт
|
||
`fatal_post_apply` — регрессия на сценарий «PHASE 0 прошла, apt-get упал,
|
||
установщик заявил, что ничего не тронул»;
|
||
- начатый bootstrap (`bootstrapTouched`) тоже даёт `fatal_post_apply`: раскладку
|
||
выполняет оркестратор, и она учитывается наравне с остальными шагами.
|
||
|
||
`orchestrator/test/install-sequence.test.ts` — порядок фаз, который иначе
|
||
проверяется только на живом сервере:
|
||
|
||
- `preflight()` в режиме install **отказывается работать без явного
|
||
`checkCleanHost`**, и отказ наступает до любой работы с системой;
|
||
- clean-host запрашивается ровно один раз за операцию и **до** первой записи
|
||
install-state — регрессия на сценарий, где повторный preflight после
|
||
`installDeps` опознавал собственный `install-state.json` как маркер чужой
|
||
установки и валил каждую чистую установку;
|
||
- проход capabilities явно отказывается от clean-host;
|
||
- `bootstrapTouched` взводится **перед** `bootstrapRuntime`, а сам bootstrap
|
||
идёт до `installDeps`;
|
||
- дальнейшая установка работает от установленного runtime-пакета;
|
||
- `diagnosticsCollect` обёрнута в `try/catch`, и `catch` стоит **до** отката:
|
||
диагностика — best effort, откат — обязателен;
|
||
- в `package/install.sh` не осталось ни одной мутирующей команды, и он
|
||
передаёт управление оркестратору через `exec`;
|
||
- классификация отказа `reconfigure`/`repair` идёт по ownership-флагам, а не по
|
||
регулярному выражению над текстом ошибки.
|
||
|
||
`orchestrator/test/install-state.test.ts`:
|
||
|
||
- маркер текущего поколения принимается;
|
||
- маркер без полей поколения отклоняется, **несмотря на `installed: true`**;
|
||
- чужой `product`, `release_line` или `config_schema_version` отклоняются;
|
||
- записываемый маркер всегда несёт идентификацию поколения;
|
||
- незавершённая установка подсказывает `repair --allow-partial-state`.
|
||
|
||
## A6. Редактирование секретов (unit)
|
||
|
||
`orchestrator/test/redaction.test.ts`:
|
||
|
||
- machine token не переживает редакцию серверного конфига — регрессия на
|
||
построчное правило `auth:`, оставлявшее нетронутым `auth.http.url`;
|
||
- obfs-пароль не переживает редакцию;
|
||
- результат остаётся валидным YAML;
|
||
- несекретные поля сохраняются: диагностика должна оставаться полезной;
|
||
- неизвестное поле с секретоподобным именем вырезается;
|
||
- `acme.dns.config` вырезается целиком;
|
||
- невалидный YAML не роняет редакцию и всё равно чистится;
|
||
- секрет внутри URL-значения в env вырезается, даже если имя ключа несекретное
|
||
(`HY2_AUTH_URL`);
|
||
- URL под **произвольным** именем ключа теряет встроенные учётные данные и
|
||
секретные query-параметры, но сохраняет адрес; то же для URL внутри списка;
|
||
- `redactLogText` вырезает machine token из строки journald, сохраняя host,
|
||
port и path; ловит секрет и вне URL; не трогает обычные строки; сохраняет
|
||
хвостовую пунктуацию; идемпотентен — регрессия на diagnostics-бандл, где
|
||
редактировались env и YAML, а `journal-admin.log` копировался как есть.
|
||
|
||
## A7. Machine token в журналах (unit)
|
||
|
||
`apps/middleware/log_test.go` — запрос
|
||
`/internal/hysteria/auth?access_token=SUPER_SECRET_SENTINEL`:
|
||
|
||
- sentinel **не появляется** в журнале ни в каком виде;
|
||
- в журнале есть `reqPath`, поля `reqUri` нет;
|
||
- имя query-параметра сохраняется (`reqQueryKeys`), значение — нет;
|
||
- пустой список параметров в журнал не пишется;
|
||
- то же правило действует на операторских маршрутах, а не только на машинном.
|
||
|
||
`apps/service/log_sanitize_test.go` — тот же санитайз на стороне админки: журнал
|
||
Hysteria покидает сервер через `ExportLog`, а `HY2_AUTH_URL` несёт
|
||
`access_token`, который upstream волен упомянуть в сообщении об ошибке.
|
||
|
||
## A8. Инвариант публичного endpoint (unit)
|
||
|
||
`orchestrator/test/network-endpoint.test.ts` — проба подменяет и DNS, и список
|
||
локальных адресов, поэтому тест не зависит ни от сети, ни от интерфейсов машины
|
||
разработчика.
|
||
|
||
| Сценарий | Результат |
|
||
| --- | --- |
|
||
| A-запись == текущий публичный IPv4 | PASS |
|
||
| A-запись == старый IPv4 | FAIL, в тексте оба адреса |
|
||
| A-запись отсутствует | FAIL |
|
||
| A == текущий + чужой | FAIL |
|
||
| у сервера 2 публичных IP, DNS использует один | PASS |
|
||
| `PUBLIC_HOST` — правильный IPv4-литерал | PASS |
|
||
| `PUBLIC_HOST` — устаревший IPv4-литерал | FAIL |
|
||
| `DOMAIN` совпадает, отдельный `PUBLIC_HOST` устарел | FAIL |
|
||
| `PUBLIC_HOST` совпадает, отдельный TLS-домен устарел | FAIL |
|
||
| нет ни одного локального публичного IPv4 | FAIL |
|
||
| `HY2XS_PUBLIC_ENDPOINT_POLICY` = strict / warn / off | fail / warn / skip |
|
||
| отсутствие A-записи при любой политике | FAIL |
|
||
| отказ резолвера (SERVFAIL/таймаут/отказ) при любой политике | FAIL, отдельный текст |
|
||
|
||
Отдельно проверяется классификация IPv4. Список исключений приведён к IANA
|
||
Special-Purpose Address Registry: приватные, CGNAT, link-local, multicast,
|
||
reserved, benchmarking (`198.18/15`), 6to4-anycast и **документационные**
|
||
диапазоны (`192.0.2/24`, `198.51.100/24`, `203.0.113/24`) не считаются
|
||
публичным адресом сервера. Регрессия: `203.0.113.5` из RFC-примеров раньше
|
||
проходил проверку как обычный публичный адрес. Границы проверяются с обеих
|
||
сторон — `172.32.0.0`, `192.0.1.1`, `198.20.0.1` и `203.0.112.255` считаются
|
||
публичными.
|
||
|
||
Отказ резолвера отделён от отсутствия записи: `ENODATA`/`ENOTFOUND`/`NXDOMAIN`
|
||
— это «нет A-записи» и чинится в DNS-панели, всё остальное — «резолвер не
|
||
ответил» и чинится в `/etc/resolv.conf`. Раньше оба случая печатались как
|
||
«has no A-record», и при сломанном резолвере оператор шёл править запись,
|
||
которая была на месте. Фатальны оба: без ответа резолвера проверка не выполнена,
|
||
а не «выполнена с замечанием».
|
||
|
||
## A9. Регистрация маршрутов (unit)
|
||
|
||
`apps/router/router_test.go` — единственное место, где ошибка проявляется
|
||
**паникой при старте сервиса**, а не ответом с кодом. Конфликт с
|
||
wildcard-маршрутом фронтенда или дублирующая регистрация обнаружились бы иначе
|
||
только на живом сервере.
|
||
|
||
- контур маршрутов собирается без паники;
|
||
- machine-auth зарегистрирован ровно на `constant.HysteriaMachineAuthPath`;
|
||
- операторский и auth API — под `constant.AdminAPIBase`;
|
||
- ни один маршрут не начинается со старого пространства имён;
|
||
- удалённые маршруты (включая `exportConfig`/`importConfig` и `getConfig`) не
|
||
вернулись;
|
||
- пространство `/api/config` закрыто: в нём ровно четыре маршрута, и любой
|
||
новый обязан быть добавлен в тест осознанно;
|
||
- `/healthz` на месте.
|
||
|
||
## A9a. Доступ к таблице `config` (unit)
|
||
|
||
`apps/model/constant/config_test.go` — allowlist как структура, а не как
|
||
соглашение:
|
||
|
||
- ни один внутренний ключ не читается и не записывается через API;
|
||
- `JWT_SECRET`, `PEER_SECRET_KEY`, `PEER_SECRET_ENCRYPTION_KEY` и
|
||
`HYSTERIA2_TRAFFIC_STATS_SECRET` поимённо объявлены внутренними;
|
||
- пользовательские настройки остаются доступными;
|
||
- множество записываемых ключей — подмножество читаемых;
|
||
- неизвестный ключ закрыт **по умолчанию**: забытый при denylist ключ был бы
|
||
сразу публичным.
|
||
|
||
`apps/controller/config_test.go` — то же на уровне HTTP:
|
||
|
||
- чтение и запись каждого секрета отклоняются;
|
||
- секрет, спрятанный среди разрешённых ключей, отклоняет весь запрос;
|
||
- отказ наступает **до** обращения к базе (тест работает без SQLite — сам факт,
|
||
что обработчик не падает, это и доказывает);
|
||
- ключи оркестратора отклоняются с указанием владельца, а не общим «нет такого
|
||
ключа»: оператор должен быть отправлен к `hy2xs-orchestrator reconfigure`;
|
||
- удалённые ключи (`HYSTERIA2_ENABLE`, `HYSTERIA2_CONFIG`,
|
||
`HYSTERIA2_TRAFFIC_TIME`, `HYSTERIA2_CONFIG_REMARK`) отклоняются как
|
||
неизвестные — проверка идёт по строковым литералам, потому что соответствующих
|
||
констант в коде уже нет и появиться они не должны.
|
||
|
||
Атомарность партии проверяется на **настоящей** SQLite: без базы утверждение
|
||
«партия не применилась частично» бессмысленно, поскольку предметом утверждения
|
||
является именно состояние базы.
|
||
|
||
- разрешённый ключ первым, запрещённый вторым → запрос отклонён, значение
|
||
первого ключа в базе **не изменилось**;
|
||
- невалидное cron-выражение → отказ, значение в базе не изменилось;
|
||
- один ключ дважды в партии → отказ (какое из двух значений считать намерением
|
||
оператора, определить нельзя);
|
||
- корректная партия → значение сохранено, расписание применено к планировщику,
|
||
число его записей не выросло.
|
||
|
||
Порядок в первом тесте принципиален. Предыдущая версия ставила запрещённый ключ
|
||
**первым** и до второго элемента не доходила, поэтому проходила и на реализации,
|
||
которая проверяла и записывала настройки в одном цикле.
|
||
|
||
## A9b. Планировщик (unit)
|
||
|
||
`apps/service/cron_scheduler_test.go` — планировщик как собственность процесса:
|
||
|
||
- четыре последовательные смены расписания **не увеличивают** число записей
|
||
планировщика (главная регрессия: раньше каждая смена добавляла целый
|
||
дублирующий набор джоб, а старое расписание продолжало работать);
|
||
- пустое выражение снимает джобу сброса, непустое возвращает её — без
|
||
перезапуска процесса;
|
||
- невалидное выражение не меняет планировщик и не снимает действующую джобу;
|
||
- набор валидных и невалидных выражений проверяется тем же парсером, что и
|
||
runtime: то, что `cron` умеет, обязано приниматься, остальное — отклоняться;
|
||
- невалидное значение в базе **не роняет старт**: на панели висит
|
||
`/internal/hysteria/auth`, и отказ старта из-за строки расписания положил бы
|
||
подключения пользователей. Фиксированные джобы поднимаются, сброс отключён,
|
||
в журнале ERROR, и настройка чинится через API без перезапуска;
|
||
- второй `InitCron` поверх работающего отклоняется;
|
||
- `StopCron` идемпотентен и оставляет планировщик пустым.
|
||
|
||
## A9c. Токены и пароли (unit)
|
||
|
||
`apps/service/jwt_test.go`:
|
||
|
||
- round-trip: claims, включая `token_version`, переживают выписку и разбор;
|
||
- токен, подписанный **другим** HMAC-алгоритмом тем же ключом, отклоняется
|
||
(прежний `keyfunc` не смотрел на `token.Method` вовсе);
|
||
- токен без `exp` отклоняется, истёкший отклоняется отдельным сообщением;
|
||
- токен с чужим `issuer` отклоняется даже при совпадении ключа;
|
||
- пустой `JWT_SECRET` — отказ и на выписку, и на разбор, а не подпись ключом
|
||
нулевой длины.
|
||
|
||
`apps/util/encrypt_test.go`:
|
||
|
||
- `HashPassword` выдаёт bcrypt и солит: два хеша одного пароля различаются;
|
||
- вход по несолёному SHA-224 (формат предыдущего поколения) **невозможен**;
|
||
- любая не-bcrypt строка в поле хеша отклоняется.
|
||
|
||
`apps/util/rand_test.go` — отсутствие modulo bias: на выборке 200 000 символов
|
||
частоты первых восьми символов алфавита не отличаются от остальных более чем на
|
||
5%. Прежняя реализация давала здесь отношение 1.25.
|
||
|
||
## A9d. Пир установщика (unit)
|
||
|
||
`apps/service/peer_bootstrap_guard_test.go` — защита действует во всех путях
|
||
записи, а не только в импорте:
|
||
|
||
- смена секрета и переименование `bootstrap-admin-peer` отклоняются, состояние
|
||
в базе не меняется;
|
||
- переименование обычного пира в зарезервированное имя отклоняется;
|
||
- создание пира с зарезервированным именем отклоняется;
|
||
- отключение и изменение квоты **разрешены**;
|
||
- удаление **разрешено**: это осознанное действие оператора, и расхождения
|
||
между базой и `bootstrap-admin.secret` оно не создаёт.
|
||
|
||
## A10. Импорт пиров (unit)
|
||
|
||
`apps/service/peer_import_test.go`:
|
||
|
||
- выгрузка, сделанная `ExportPeer`, принимается без правок;
|
||
- имя проверяется теми же правилами, что и при обычном создании пира: длина,
|
||
набор символов, отсутствие пробелов и переводов строки;
|
||
- `bootstrap-admin-peer` не может быть импортирован ни по имени, ни по `authId`:
|
||
его секрет продублирован в `/etc/hy2xs/bootstrap-admin.secret`;
|
||
- диапазоны `quotaBytes`, `expiresAt`, `maxDevices`, `disabled`, `bannedUntil`,
|
||
счётчиков трафика и длины секрета проверяются;
|
||
- sentinel-значения (`quotaBytes = -1`, `maxDevices = 0`) остаются валидными;
|
||
- дубликаты имени и `authId` внутри одной партии отклоняются;
|
||
- партия сверх лимита отклоняется;
|
||
- невалидная **последняя** запись отклоняет весь файл: импорт применяется
|
||
целиком или не применяется вовсе.
|
||
|
||
`apps/service/peer_import_tx_test.go` — та же гарантия уже на уровне базы, на
|
||
настоящей SQLite. Валидация не даёт применить испорченный файл, но она ничего
|
||
не говорит о конфликте с тем, что УЖЕ лежит в базе:
|
||
|
||
- валидная партия применяется целиком;
|
||
- **cross-conflict откатывается полностью**: пусть в базе есть
|
||
`A(auth_id=aaa, name=alice1)` и `B(auth_id=bbb, name=bob123)`, а файл несёт
|
||
`(auth_id=aaa, name=bob123)` — поиск найдёт A по `auth_id` и попытается
|
||
переименовать её в `bob123`, прямо в `UNIQUE(name)`. Записи, шедшие в файле
|
||
до конфликтной, не должны остаться применёнными. Тест дополнительно
|
||
убеждается, что отказ пришёл **из базы**, а не из валидации;
|
||
- дубликат `auth_id` на вставке ведёт себя так же;
|
||
- отказ валидации не доходит до базы вовсе;
|
||
- пир установщика защищён и внутри транзакции;
|
||
- обновление без секрета в файле не перезаписывает существующий секрет.
|
||
|
||
`apps/controller/peer_test.go` — разбор загруженного файла:
|
||
|
||
- файл с **хвостовым** JSON-документом отклоняется: `json.Decoder` читает первый
|
||
документ и останавливается, поэтому раньше оператор видел «импорт выполнен»,
|
||
а вторая половина файла молча не применялась;
|
||
- неизвестные поля и файл не с расширением `.json` отклоняются;
|
||
- корректный одиночный документ доходит до базы и создаёт пира.
|
||
|
||
## A7. Контракт версий (build)
|
||
|
||
Шаг `verify_versions_contract` (`tools/build/lib/versions.sh`) роняет сборку до
|
||
создания tarball при рассинхроне `versions.env` с `PACKAGE_VERSION`,
|
||
`packageManager` обоих `package.json`, схемой в `package/config/hy2xs.env`,
|
||
константами, скомпилированными в оркестратор, директивой `go` в `apps/go.mod`,
|
||
metadata пакета и версией, которую сообщает собранный `hy2xs-admin`.
|
||
|
||
Разбор upstream `hashes.txt` покрыт `orchestrator/test/hysteria-release.test.ts`:
|
||
реальный формат релиза, отсутствие путаницы `hysteria-linux-amd64` с
|
||
`hysteria-linux-amd64-avx`, форма `sha256:<hex>`, верхний регистр,
|
||
противоречивые записи, отсутствие нужной строки.
|
||
|
||
## A11. Проверка зависимостей на уязвимости (build)
|
||
|
||
`tools/build/lib/security.sh` — обязательный шаг между тестами админки и записью
|
||
metadata. Подробности в [docs/02](02-build-layer-and-package.md); здесь важно
|
||
поведение при отказе:
|
||
|
||
| Код `govulncheck` | Трактовка |
|
||
| --- | --- |
|
||
| `0` | чисто |
|
||
| `3` | найдены **вызываемые** уязвимости → сборка падает |
|
||
| иное | отказ самого инструмента → сборка падает отдельным сообщением |
|
||
|
||
Последняя строка существенна: ненулевой код неизвестной природы нельзя
|
||
трактовать как «уязвимостей нет». По той же причине недоступность реестра npm
|
||
для `pnpm audit` — это отказ проверки, а не её отрицательный результат.
|
||
|
||
## A12. Приёмка проверяет код, а не упоминания
|
||
|
||
Два контракта приёмки на снимке до этого патча **гарантированно роняли сборку на
|
||
корректном коде**, и оба — по одной причине: они искали подстроку там, где
|
||
подстрока обязана присутствовать.
|
||
|
||
| Проверка | Что ловила на самом деле |
|
||
| --- | --- |
|
||
| `verify_api_namespace_contract` | `grep -rlF '/hui'` возвращал `apps/router/router_test.go` (регрессионный тест, который ПЕРЕЧИСЛЯЕТ legacy-префикс, чтобы доказать его отсутствие) и сам `versions.sh`, где эта строка стоит в тексте проверки |
|
||
| «peer import не выходит за транзакцию» | `source.slice(start)` брал файл от начала `applyPeerImportEntry` **и до конца**, захватывая `ExistPeerName` и `UpdatePeerLastConnectionAt` — обычные операции вне импорта, которым глобальное соединение положено |
|
||
|
||
Первая падала на шаге versions contract — шестым из четырнадцати, до резолва
|
||
Hysteria. Вторая не была замечена только потому, что сборка до неё не доходила.
|
||
|
||
Отсюда правило и помощники `code_without_comments` / `code_mentions_in` в
|
||
`acceptance.sh`: проверка смотрит на код, а комментарий, объясняющий, почему
|
||
чего-то больше нет, обязан называть это по имени и не должен ломать сборку.
|
||
Отсутствие legacy-маршрута доказывает не `grep` по исходникам, а
|
||
`TestRouterHasNoLegacyNamespace` на таблице маршрутов собранного роутера —
|
||
и существование этого теста само проверяется контрактом.
|
||
|
||
## B. Target install tests
|
||
|
||
### На чистом Debian 13 проверяем
|
||
1. пакет запускается без ручной сборки на сервере
|
||
2. Hysteria2 скачивается с official upstream
|
||
3. bundled HY2XS admin раскладывается локально из пакета
|
||
4. создаются нужные каталоги
|
||
5. создаются systemd unit-файлы
|
||
6. создаются `hy2xs.env` и `post-install.env` с правами `0600 root:root`
|
||
7. baseline firewall применяется корректно через staged mode
|
||
8. SSH остаётся доступным
|
||
9. `reconfigure --dry-run` выводит план изменений
|
||
10. `reconfigure --apply` применяет изменения и проходит smoke
|
||
|
||
## C. Runtime tests
|
||
|
||
1. `hysteria-server` active
|
||
2. `hy2xs-admin` active
|
||
3. Hysteria слушает только IPv4 (`0.0.0.0:<udp_port>`)
|
||
4. HY2XS admin слушает ожидаемый `HY2XS_UI_BIND_HOST:<ui_port>`
|
||
5. тестовый совместимый клиент подключается
|
||
6. идёт реальный трафик
|
||
7. лимит 50/50 Mbps соблюдается при согласованной клиентской конфигурации
|
||
8. reboot не ломает baseline
|
||
9. Hysteria2 управляется systemd unit, а не внутренним updater'ом admin panel
|
||
10. нет IPv6 listen (`[::]`) для Hysteria/HY2XS admin
|
||
11. `trafficStats.secret` не равен `JWT_SECRET`
|
||
12. bootstrap admin secret существует и имеет `0600`
|
||
13. `trafficStats` API: корректный secret принимает запрос, неверный secret отклоняется
|
||
14. TLS mode в `config.yaml` соответствует runtime env (`acme|file|self_signed_dev`)
|
||
15. при `HY2XS_TLS_MODE=acme` в `config.yaml` выставлен `acme.type` из `HY2XS_ACME_TYPE`
|
||
16. direct `hysteria2://` node URL в API/QR формируется по `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT`; subscription delivery endpoint отключён в baseline и не входит в acceptance
|
||
17. `nft -c -f /etc/nftables.conf` проходит после apply
|
||
18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin`
|
||
19. остановка/рестарт UI не останавливает `hysteria-server`
|
||
20. traffic accounting/kick ориентируются на systemd status; ключа `HYSTERIA2_ENABLE` в базе больше нет
|
||
21. `/etc/hysteria/config.yaml` имеет `0640 hysteria:hy2xs-admin`
|
||
22. `hy2xs-admin` может читать `/etc/hysteria/config.yaml`, но не может писать
|
||
23. смена расписания сброса трафика применяется **без** перезапуска `hy2xs-admin`, и число джоб планировщика не растёт
|
||
24. невалидное cron-выражение отклоняется API, а значение в базе не меняется
|
||
25. `systemctl restart hy2xs-admin` завершает сервис штатно: планировщик остановлен до закрытия SQLite, в журнале нет `database is closed`
|
||
26. `govulncheck ./...` на графе релиза не находит вызываемых уязвимостей
|
||
|
||
## C1. Семантический smoke конфига
|
||
|
||
Недостаточно `grep` по YAML: он не отличит нужное поле от такой же строки в другой секции и не заметит оставшийся рядом лишний подблок.
|
||
|
||
Smoke разбирает `/etc/hysteria/config.yaml` и сверяет с production-профилем:
|
||
|
||
```text
|
||
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
|
||
окна, maxIncomingStreams, disablePathMTUDiscovery == baseline
|
||
maxIdleTimeout == 30s
|
||
|
||
trafficStats:
|
||
listen == runtime env
|
||
secret непустой
|
||
|
||
auth:
|
||
type == http
|
||
url == http://127.0.0.1:<UI_PORT>/internal/hysteria/auth?access_token=<machine token>
|
||
insecure == (tlsMode == self_signed_dev)
|
||
|
||
TLS:
|
||
acme-режим не содержит секции tls
|
||
acme: type/email/ca/dir/listenHost/первый домен == профиль
|
||
file-режим не содержит секции acme
|
||
|
||
верхний уровень:
|
||
нет секций вне production-профиля
|
||
```
|
||
|
||
`maxIdleTimeout` присутствовал в профиле, но не проверялся: конфиг с уехавшим
|
||
idle timeout проходил семантическую проверку. Точно так же `auth.http.url`
|
||
раньше сверялся только на наличие подстроки `access_token=`, из-за чего
|
||
уехавший порт или путь остались бы незамеченными — а это единственный канал
|
||
допуска пиров.
|
||
|
||
Сообщение об ошибке для `auth.http.url` намеренно не печатает сам токен: текст
|
||
уходит в логи и в diagnostics-бандл. Это закреплено отдельным тестом.
|
||
|
||
## C2. End-to-end с реальным клиентом
|
||
|
||
`tools/test/e2e-hysteria.sh`, отдельно для Gecko и Salamander:
|
||
|
||
1. сервер принимает сгенерированный конфиг и стартует;
|
||
2. TLS handshake;
|
||
3. handshake с обфускацией;
|
||
4. HTTP auth HY2XS: разрешённый пир принят;
|
||
5. HTTP auth HY2XS: неразрешённый пир отклонён;
|
||
6. клиент подключается **именно по ссылке, которую выдаёт production-код**;
|
||
7. TCP forwarding;
|
||
8. UDP forwarding;
|
||
9. `trafficStats` с валидным secret;
|
||
10. `trafficStats` с невалидным secret отклоняется;
|
||
11. per-peer accounting содержит аутентифицированного пира;
|
||
12. перезапуск сервера;
|
||
13. быстрое переподключение клиента (поведение stateless reset).
|
||
|
||
Пункт 6 — тот самый, который ловит класс ошибок, неизбежный при наивном включении Gecko: сервер работает, ссылка формально валидна, а клиент по ней не подключается.
|
||
|
||
### Одна реализация URI, а не две
|
||
|
||
Ссылка берётся из production-генератора через `apps/tools/share-uri`, который
|
||
вызывает ту же `service.BuildHysteria2ShareURI`, что и панель.
|
||
|
||
Раньше внутри e2e жила **вторая** реализация URI на bash. Go-юнит-тесты
|
||
проверяли production-генератор, e2e проверял свою функцию — и дрейф любой из
|
||
них оставлял обе группы тестов зелёными.
|
||
|
||
Единственное расхождение с пользовательской ссылкой — `insecure=1`: e2e
|
||
работает на самоподписанном сертификате. Это расхождение ограничено с двух
|
||
сторон:
|
||
|
||
- e2e отдельно печатает и проверяет **production-вариант** ссылки
|
||
(`insecure=0`, корректные `obfs` и `sni`);
|
||
- Go-тест `TestBuildHysteria2ShareURI_InsecureDiffersOnlyInThatParam`
|
||
доказывает, что кроме этого параметра ссылки совпадают побайтово;
|
||
- Go-тест `TestBuildHysteria2Url_ProductionPathNeverDisablesVerification`
|
||
фиксирует, что production-путь никогда не передаёт `insecure=1`.
|
||
|
||
Для запуска e2e нужен Go (`GO_BIN`).
|
||
|
||
## 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;
|
||
- вырезается **неизвестное** поле с секретным именем;
|
||
- URL под произвольным именем ключа (`endpoint:`) теряет учётные данные и
|
||
`access_token`, но сохраняет адрес; то же для URL внутри списка;
|
||
- не-URL скаляры (`50 mbps`, `0.0.0.0:443`, `10.0.0.1:1080`, `30s`, числа)
|
||
проходят санитайзер без изменений;
|
||
- пути к файлам (`tls.key`, `ech.keyPath`, `clientCA`) остаются видимыми.
|
||
|
||
Го- и TS-санитайзеры описывают один контракт и покрыты зеркальными тестами:
|
||
граница определяется значением, а не именем ключа.
|
||
|
||
## D0. Граница установки на живом сервере
|
||
|
||
Проверяется на хосте, где уже стоит предыдущая установка:
|
||
|
||
1. `install.sh` завершается отказом на PHASE 0;
|
||
2. `/usr/local/lib/hy2xs` **не создан и не изменён**;
|
||
3. `/var/lib/hy2xs/install-state.json` не перезаписан;
|
||
4. `hysteria-server` и `hy2xs-admin` остались `active`;
|
||
5. в тексте отказа перечислены найденные маркеры и указан
|
||
`docs/14-legacy-cleanup.md`;
|
||
6. после `tools/legacy/purge-v0.sh --apply --yes-i-know` установка проходит.
|
||
|
||
Пункты 2–4 — прямая регрессия: прежний установщик успевал переписать
|
||
`/usr/local/lib/hy2xs` и `install-state.json`, а затем откатом останавливал и
|
||
выключал работающие службы старой установки.
|
||
|
||
## D1. Отказ сразу после успешной PHASE 0 (fault injection)
|
||
|
||
Проверяется на чистом хосте. Это узкая щель между «PHASE 0 прошла» и «первая
|
||
мутирующая операция упала» — место, где установщик раньше врал.
|
||
|
||
1. PHASE 0 проходит успешно;
|
||
2. `installDeps` ломается искусственно (например, недоступный apt-репозиторий
|
||
или временно испорченный `/etc/apt/sources.list.d/`);
|
||
3. установка завершается отказом;
|
||
4. в выводе **нет** `fatal_pre_apply` и нет фразы про «ничего не применялось»;
|
||
5. `/var/lib/hy2xs/install-state.json` существует и честно показывает
|
||
`phase: failed` с текстом ошибки;
|
||
6. `owned_paths` в маркере содержит `/usr/local/lib/hy2xs`,
|
||
`/usr/local/bin/hy2xs-orchestrator` и `/usr/local/lib/hy2xs/package` —
|
||
всё, что операция действительно создала;
|
||
7. diagnostics-бандл собран;
|
||
8. `hy2xs-orchestrator status` не заявляет установку успешной.
|
||
|
||
До исправления шаги 4–7 давали противоположный результат: `install-state.json`
|
||
уже лежал на диске, но отказ классифицировался как pre-apply, обработка
|
||
состояния пропускалась, а следующая установка на этой машине отказывалась по
|
||
clean-host контракту из-за оставшегося маркера.
|
||
|
||
Пункт 6 закрывает вторую половину той же щели. Пока раскладку оркестратора и
|
||
runtime-пакета выполнял `install.sh`, эти пути не принадлежали никому: они не
|
||
попадали в `owned_paths`, а отказ **второго** preflight (сменился DNS, занялся
|
||
порт, не ответил резолвер) объявлялся `fatal_pre_apply` — «на сервере ничего не
|
||
изменено» — при уже созданном каталоге оркестратора.
|
||
|
||
## D1a. Проход установки не спотыкается о собственный маркер
|
||
|
||
Проверяется на чистом хосте, обычной успешной установкой.
|
||
|
||
1. `install.sh` доходит до `preflight capabilities` **после** `apt-get`;
|
||
2. установка на этом шаге **не** падает с текстом «обнаружена предыдущая или
|
||
посторонняя установка»;
|
||
3. установка доходит до `installed`.
|
||
|
||
Это сценарий, который не воспроизводится ни на одном dry-run: clean-host внутри
|
||
`install` проверялся дважды, и ко второму разу на диске уже лежал собственный
|
||
`/var/lib/hy2xs/install-state.json`, записанный после первого preflight. Каждая
|
||
чистая установка падала сразу после `apt-get`, получала `fatal_post_apply` и
|
||
оставляла сервер наполовину настроенным. Структурно закреплено в
|
||
`orchestrator/test/install-sequence.test.ts`.
|
||
|
||
## D2. Устаревший DNS после смены IPv4 провайдером
|
||
|
||
Проверяется на рабочей установке.
|
||
|
||
```text
|
||
сервер: текущий публичный IPv4 = B
|
||
DNS: A-запись = A (старый адрес)
|
||
|
||
hy2xs-orchestrator doctor
|
||
→ FAIL
|
||
→ в выводе присутствуют и A, и B
|
||
|
||
обновить A-запись на B, дождаться TTL
|
||
|
||
hy2xs-orchestrator doctor
|
||
→ PASS
|
||
```
|
||
|
||
Дополнительно: `reconfigure --apply` при устаревшей A-записи тоже обязан
|
||
отказать — инвариант живёт в общем `preflight`, а не в одном `doctor`.
|
||
|
||
## D. Negative tests
|
||
|
||
1. не Debian 13
|
||
2. порт уже занят
|
||
3. старое конфликтующее состояние уже существует
|
||
4. домен / SNI заданы некорректно
|
||
5. bundled UI отсутствует в пакете
|
||
6. Hysteria upstream недоступен
|
||
7. firewall применился частично
|
||
8. install flow прерван посередине
|
||
9. попытка использовать `HY2XS_IPV6_ENABLED=true`
|
||
10. `HY2XS_PUBLIC_HOST=0.0.0.0`
|
||
11. неизвестный `HY2XS_HYSTERIA_OBFS_TYPE`
|
||
12. конфигурация со схемой `HY2XS_CONFIG_SCHEMA_VERSION` из линейки `0.x`
|
||
13. upstream `latest` несовместим с шаблоном HY2XS — падает сборка, не установка
|
||
14. `HY2XS_PUBLIC_HOST` резолвится не на этот сервер
|
||
15. `HY2XS_DOMAIN` резолвится не на этот сервер при отличном от него `PUBLIC_HOST`
|
||
16. A-запись содержит правильный адрес и чужой одновременно
|
||
17. неизвестное значение `HY2XS_PUBLIC_ENDPOINT_POLICY`
|
||
18. импорт пиров с невалидной записью — файл не применяется частично
|
||
19. импорт пиров, пытающийся перезаписать `bootstrap-admin-peer`
|
||
|
||
## E. Fix20 production matrix (обязательные сценарии)
|
||
|
||
1. **Clean Debian 13 minimal**:
|
||
- только SSH, без ручной установки зависимостей;
|
||
- default `/etc/nftables.conf` stub;
|
||
- install проходит полностью;
|
||
- `doctor`/`status` показывают рабочее состояние.
|
||
|
||
2. **Non-systemd container**:
|
||
- fail-fast до destructive шагов;
|
||
- диагностическое сообщение с причиной capability/systemd.
|
||
|
||
3. **Foreign nftables**:
|
||
- при `HY2XS_FIREWALL_MODE=managed` install/reconfigure блокируются;
|
||
- при `HY2XS_FIREWALL_MODE=takeover` создаются backup/rollback guard и apply проходит.
|
||
|
||
4. **Rollback guard cleanup**:
|
||
- после успешного apply/smoke не остаются `hy2xs-fw-rollback-*.timer/.service`.
|
||
|
||
5. **Partial install + repair**:
|
||
- состояние `install-state` фиксирует промежуточную фазу;
|
||
- `repair` завершает граф до `installed=true`.
|
||
|
||
6. **AAAA при IPv4-only**:
|
||
- policy строго валидируется preflight;
|
||
- soft warning path не используется в production baseline.
|
||
|
||
7. **Slow-start admin readiness**:
|
||
- install не падает на race после restart;
|
||
- readiness waiters дожидаются listener/healthz.
|
||
|
||
8. **Отказ между PHASE 0 и первой мутацией** (сценарий D1):
|
||
- `install-state.json` честно показывает `failed`;
|
||
- установщик не заявляет, что хост не изменён.
|
||
|
||
9. **Устаревший DNS после смены IPv4** (сценарий D2):
|
||
- `doctor` и `reconfigure` отказывают;
|
||
- в выводе присутствуют оба адреса.
|
||
|
||
## Acceptance criteria
|
||
|
||
Система принимается, если:
|
||
|
||
1. production builder на Debian 13 amd64 выдаёт переносимый install package
|
||
2. target server не выполняет build step
|
||
3. Hysteria2 получена из official upstream
|
||
4. HY2XS admin поставлен из install package
|
||
5. `post-install.env` отражает фактическое deploy-состояние
|
||
6. оркестратор зафиксирован как Bun/TypeScript stack и поставляется как готовый install-артефакт
|
||
7. оркестратор не требует standalone update / rollback / uninstall subcommands
|
||
8. bounded rollback в install/reconfigure корректно отрабатывает failure-сценарии firewall/systemd/config/smoke
|
||
9. Telegram/access layer не требуется для прохождения install acceptance
|
||
10. отсутствует production path для port hopping
|
||
11. UI не запускается от root
|
||
12. клиентские endpoint не зависят от request `Host`/`hostname`
|
||
13. production build verify падает, если `config/hy2xs.env` содержит placeholder-значения
|
||
14. production build verify падает при dirty git tree (кроме `ALLOW_DIRTY_BUILD=true`)
|
||
15. metadata содержит `source_git_commit`, `dirty_tree`, `build_profile=production`
|
||
16. builder без override на сегодняшний день автоматически выбирает последнюю стабильную версию Hysteria
|
||
17. собранный пакет содержит **точные** версию, URL и SHA-256
|
||
18. выход новой версии Hysteria после сборки не меняет содержимое старого пакета
|
||
19. новая установка генерирует Gecko
|
||
20. Gecko использует `512/1200`
|
||
21. установленная Hysteria реально принимает сгенерированный YAML
|
||
22. сервис запускается под существующим непривилегированным пользователем `hysteria`
|
||
23. созданный пользователь получает `hysteria2://` с `obfs=gecko` и `obfs-password`
|
||
24. совместимый клиент Hysteria подключается напрямую по этой ссылке
|
||
25. после перезапуска Hysteria клиент быстро восстанавливает соединение
|
||
26. режим `HY2XS_HYSTERIA_OBFS_TYPE=salamander` полностью работоспособен
|
||
27. admin читает Gecko-конфиг без ошибок
|
||
28. экспорт не уничтожает современные и неизвестные upstream-поля
|
||
29. экспорт не содержит секретов
|
||
30. frontend отображает Gecko
|
||
31. `namedotcom` удалён, актуальные ACME-провайдеры отражены
|
||
32. документация нигде не утверждает, что Salamander — фиксированный инвариант
|
||
33. документация не фиксирует конкретный номер версии как «текущую версию», а объясняет latest-stable build policy
|
||
34. форма создания пира содержит примеры значений и пояснения для полей «Пир», «Комментарий» и «Секрет»
|