Ошибка в регистрации маршрутов проявляется паникой при старте сервиса, а не ответом с кодом: конфликт с wildcard фронтенда или дублирующая регистрация обнаружились бы только на живом сервере. Смена namespace относится ровно к этому классу изменений.
35 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; нужен 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
Проверяем
- 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 |
отклонено с указанием на чистую установку |
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: два списка описывают одну границу и не имеют права разъезжаться; - пути, созданные
install.shмежду фазами (/usr/local/lib/hy2xs,/usr/local/lib/hy2xs/package,/usr/local/bin/hy2xs-orchestrator), — маркеры в PHASE 0, но не в PHASE 1; - сообщение перечисляет найденные маркеры и говорит, что хост не изменён.
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 упал, установщик заявил, что ничего не тронул».
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 внутри списка.
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 |
Отдельно проверяется классификация IPv4: приватные, CGNAT, link-local,
multicast и reserved диапазоны не считаются публичным адресом сервера, а
172.32.0.0 и 172.15.255.255 — считаются (границы 172.16/12).
A9. Регистрация маршрутов (unit)
apps/router/router_test.go — единственное место, где ошибка проявляется
паникой при старте сервиса, а не ответом с кодом. Конфликт с
wildcard-маршрутом фронтенда или дублирующая регистрация обнаружились бы иначе
только на живом сервере.
- контур маршрутов собирается без паники;
- machine-auth зарегистрирован ровно на
constant.HysteriaMachineAuthPath; - операторский и auth API — под
constant.AdminAPIBase; - ни один маршрут не начинается со старого пространства имён;
- удалённые маршруты (включая
exportConfig/importConfig) не вернулись; /healthzна месте.
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внутри одной партии отклоняются; - партия сверх лимита отклоняется;
- невалидная последняя запись отклоняет весь файл: импорт применяется целиком или не применяется вовсе.
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>, верхний регистр,
противоречивые записи, отсутствие нужной строки.
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
окна, 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:
- сервер принимает сгенерированный конфиг и стартует;
- TLS handshake;
- handshake с обфускацией;
- HTTP auth HY2XS: разрешённый пир принят;
- HTTP auth HY2XS: неразрешённый пир отклонён;
- клиент подключается именно по ссылке, которую выдаёт production-код;
- TCP forwarding;
- UDP forwarding;
trafficStatsс валидным secret;trafficStatsс невалидным secret отклоняется;- per-peer accounting содержит аутентифицированного пира;
- перезапуск сервера;
- быстрое переподключение клиента (поведение 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. Граница установки на живом сервере
Проверяется на хосте, где уже стоит предыдущая установка:
install.shзавершается отказом на PHASE 0;/usr/local/lib/hy2xsне создан и не изменён;/var/lib/hy2xs/install-state.jsonне перезаписан;hysteria-serverиhy2xs-adminосталисьactive;- в тексте отказа перечислены найденные маркеры и указан
docs/14-legacy-cleanup.md; - после
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 прошла» и «первая мутирующая операция упала» — место, где установщик раньше врал.
- PHASE 0 проходит успешно;
installDepsломается искусственно (например, недоступный apt-репозиторий или временно испорченный/etc/apt/sources.list.d/);- установка завершается отказом;
- в выводе нет
fatal_pre_applyи нет фразы про «ничего не применялось»; /var/lib/hy2xs/install-state.jsonсуществует и честно показываетphase: failedс текстом ошибки;- diagnostics-бандл собран;
hy2xs-orchestrator statusне заявляет установку успешной.
До исправления шаги 4–6 давали противоположный результат: install-state.json
уже лежал на диске, но отказ классифицировался как pre-apply, обработка
состояния пропускалась, а следующая установка на этой машине отказывалась по
clean-host контракту из-за оставшегося маркера.
D2. Устаревший DNS после смены IPv4 провайдером
Проверяется на рабочей установке.
сервер: текущий публичный 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
- не 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 — падает сборка, не установка HY2XS_PUBLIC_HOSTрезолвится не на этот серверHY2XS_DOMAINрезолвится не на этот сервер при отличном от негоPUBLIC_HOST- A-запись содержит правильный адрес и чужой одновременно
- неизвестное значение
HY2XS_PUBLIC_ENDPOINT_POLICY - импорт пиров с невалидной записью — файл не применяется частично
- импорт пиров, пытающийся перезаписать
bootstrap-admin-peer
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.
-
Отказ между PHASE 0 и первой мутацией (сценарий D1):
install-state.jsonчестно показываетfailed;- установщик не заявляет, что хост не изменён.
-
Устаревший DNS после смены IPv4 (сценарий D2):
doctorиreconfigureотказывают;- в выводе присутствуют оба адреса.
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
- форма создания пира содержит примеры значений и пояснения для полей «Пир», «Комментарий» и «Секрет»