#!/usr/bin/env bash set -euo pipefail # code_without_comments отдаёт содержимое файла без строк-комментариев. # # Приёмка обязана проверять КОД, а не упоминания. Комментарий, объясняющий, # почему чего-то больше нет, обязан называть это по имени — иначе он бесполезен, # — и проверка, ищущая просто подстроку, начинает падать ровно на той # документации, которая описывает выполненную ею же работу. # # Урок в этом файле оплачен уже пять раз: скан versions contract ловил сам себя # на строке '/hui'; dead-route скан падал на router_test.go, который ПЕРЕЧИСЛЯЕТ # удалённые маршруты, чтобы доказать их отсутствие; скан иконок падал на # блочном комментарии, объясняющем замену плагина; скан прежних имён раннеров — # на слове `systemd-run` в прозе; скан `cancelFirewallRollback` — на # комментарии, описывающем её разделение. # # Отсюда правило для КАЖДОЙ отрицательной проверки по дереву исходников: # # формулируй синтаксическую форму, а не подстроку. # # Вызов — это имя со скобкой или с обратной кавычкой; импорт — это `import` со # спецификатором; зависимость — это ключ в package.json. Прозаическое # упоминание удалённой вещи обязано быть разрешено: комментарий, объясняющий, # почему чего-то больше нет, называет это по имени — иначе он бесполезен. # # Ограничение помощника, о котором важно помнить: он отбрасывает СТРОЧНЫЕ # комментарии (`//`, `#`), но НЕ блочные `/* ... */`. Блок-парсер здесь # сознательно не заводится: наивный стриппер спотыкается о `/*` внутри строк и # регулярных выражений и может вычистить настоящий код, а это ложный PASS — # отказ хуже болезни. Для файлов с блочными комментариями формулируйте # синтаксическую форму либо опирайтесь на более сильный гейт (сборка bundle, # компилятор, тесты). # # Вид комментария выбирается по расширению файла. # # Раньше отбрасывались только строки, начинающиеся с `//`, поэтому сканы по # tools/build/*.sh не работали вовсе: там комментарии начинаются с решётки, и # объяснение «почему этого больше нет» считалось за код. # # Решётка при этом отбрасывается ТОЛЬКО в shell. В шаблоне Vue строка вполне # может начинаться с `#default="scope"` — это сокращение для v-slot, а не # комментарий, и общий фильтр молча выбрасывал бы её из скана. # # Несуществующий путь — ОТКАЗ, а не пустой вывод. Прежнее `2>/dev/null || true` # превращало опечатку в имени файла в пустую строку, а пустая строка для # проверок вида «запрещённой конструкции здесь нет» означает успех. Скан, # который молча проверяет пустоту, хуже отсутствующего: он отчитывается зелёным. code_without_comments() { [ -f "$1" ] || fail "code_without_comments: файл не найден: $1" case "$1" in # grep -v возвращает 1, когда КАЖДАЯ строка файла — комментарий. Это # законный результат, а не отказ, поэтому статус здесь гасится. *.sh) grep -vE '^[[:space:]]*#' "$1" || true ;; *) grep -v '^[[:space:]]*//' "$1" || true ;; esac } # code_has / code_lacks — поиск по КОДУ файла без пайплайна. # # Пайплайн здесь был ловушкой. Поиск с флагом -q прекращает чтение на первом # совпадении, продюсер получает SIGPIPE, и под `pipefail` статус всей # конструкции становится 141 — то есть НЕНУЛЕВЫМ именно тогда, когда совпадение # НАЙДЕНО. Для отрицательных проверок («такой конструкции в коде нет») это # давало бы ложный PASS: найденная запрещённая конструкция читалась бы как её # отсутствие. Здесь беду удерживало `|| true` внутри помощника, гасившее 141, — # то есть корректность гейта держалась на побочном эффекте подавления ошибок, # а не на чём-то заявленном. # # Отказ чтения проверяется ЯВНО, а не через errexit. В контексте `! code_has …` # bash отключает errexit на весь вызов, поэтому неудачное чтение прошло бы # незамеченным и снова дало бы ложный PASS. `fail` выполняется в самой функции, # а не в подоболочке подстановки, поэтому его `exit` останавливает сборку. code_has() { local file="$1" shift local code code="$(code_without_comments "$file")" \ || fail "acceptance: не удалось прочитать код файла $file" grep -q "$@" <<<"$code" } # code_mentions_in отдаёт список файлов, где подстрока встречается В КОДЕ. code_mentions_in() { local needle="$1" shift local file hits="" while IFS= read -r file; do [ -n "$file" ] || continue if code_has "$file" -F -- "$needle"; then hits="${hits}${hits:+ }${file}" fi done </dev/null || true) EOF printf '%s' "$hits" } run_fix20_acceptance_subset() { local package_dir="$1" [ -d "$package_dir" ] || fail "acceptance: package dir not found: $package_dir" log_step "Acceptance: package layout sanity" [ -x "$package_dir/install.sh" ] || fail "acceptance: install.sh is missing or not executable" [ -x "$package_dir/orchestrator/hy2xs-orchestrator" ] || fail "acceptance: orchestrator artifact is missing" log_step "Acceptance: project license is shipped with the package" [ -f "$package_dir/LICENSE" ] || fail "acceptance: LICENSE is missing from the package" grep -q 'GNU AFFERO GENERAL PUBLIC LICENSE' "$package_dir/LICENSE" \ || fail "acceptance: packaged LICENSE must be AGPL-3.0-only" grep -q '^license=AGPL-3.0-only$' "$package_dir/metadata/package.env" \ || fail "acceptance: package metadata must declare license=AGPL-3.0-only" log_step "Acceptance: orchestrator CLI help path" "$package_dir/orchestrator/hy2xs-orchestrator" diagnostics collect --package-dir "$package_dir" >/dev/null 2>&1 || true log_step "Acceptance: firewall mode defaults in config" grep -q '^HY2XS_FIREWALL_MODE=' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FIREWALL_MODE missing in runtime config" log_step "Acceptance: baseline domain/public host/ssh defaults" grep -q '^HY2XS_DOMAIN=fi.api.withen.pro$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_DOMAIN must default to fi.api.withen.pro" grep -q '^HY2XS_PUBLIC_HOST=fi.api.withen.pro$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_PUBLIC_HOST must default to fi.api.withen.pro" grep -q '^HY2XS_SSH_PORT=2323$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_SSH_PORT must default to 2323" log_step "Acceptance: force password change production default" grep -q '^HY2XS_FORCE_PASSWORD_CHANGE=false$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FORCE_PASSWORD_CHANGE must default to false" log_step "Acceptance: config schema version is declared" # Значение берётся из versions.env, а не пишется числом: захардкоженная # двойка означала бы, что при переходе на schema 3 нужно помнить ещё и про # эту строку. Источник истины у схемы ровно один. grep -q "^HY2XS_CONFIG_SCHEMA_VERSION=${HY2XS_CONFIG_SCHEMA_VERSION}\$" "$package_dir/config/hy2xs.env" \ || fail "acceptance: packaged baseline must declare HY2XS_CONFIG_SCHEMA_VERSION=${HY2XS_CONFIG_SCHEMA_VERSION}" log_step "Acceptance: public endpoint policy is declared and strict by default" grep -q '^HY2XS_PUBLIC_ENDPOINT_POLICY=strict$' "$package_dir/config/hy2xs.env" \ || fail "acceptance: packaged baseline must default to HY2XS_PUBLIC_ENDPOINT_POLICY=strict" log_step "Acceptance: fresh install defaults to Gecko obfuscation" grep -q '^HY2XS_HYSTERIA_OBFS_TYPE=gecko$' "$package_dir/config/hy2xs.env" \ || fail "acceptance: fresh installations must default to HY2XS_HYSTERIA_OBFS_TYPE=gecko" log_step "Acceptance: obfs type is not hardcoded in the Hysteria template" grep -q '{{OBFS_BLOCK}}' "$package_dir/templates/hysteria/config.yaml.tpl" \ || fail "acceptance: hysteria template must render the obfs block from the orchestrator" ! grep -Eq '^\s*type:\s*(gecko|salamander)\s*$' "$package_dir/templates/hysteria/config.yaml.tpl" \ || fail "acceptance: hysteria template must not hardcode an obfs type" log_step "Acceptance: modern server baseline is present in the template" grep -q 'disableLossCompensation: {{DISABLE_LOSS_COMPENSATION}}' "$package_dir/templates/hysteria/config.yaml.tpl" \ || fail "acceptance: bandwidth.disableLossCompensation missing from hysteria template" grep -q '{{CONGESTION_BLOCK}}' "$package_dir/templates/hysteria/config.yaml.tpl" \ || fail "acceptance: congestion block missing from hysteria template" grep -q '{{QUIC_BLOCK}}' "$package_dir/templates/hysteria/config.yaml.tpl" \ || fail "acceptance: quic block missing from hysteria template" log_step "Acceptance: post-install env derives obfs type from resolved context" grep -q '^HY2_OBFS_TYPE={{OBFS_TYPE}}$' "$package_dir/templates/env/post-install.env.tpl" \ || fail "acceptance: post-install env must render the resolved obfs type, not a second set of defaults" ! grep -Eq '^HY2_OBFS_TYPE=(gecko|salamander)$' "$package_dir/templates/env/post-install.env.tpl" \ || fail "acceptance: post-install env must not hardcode an obfs type" log_step "Acceptance: production defaults are declared in exactly one module" grep -q 'DEFAULT_HYSTERIA_OBFS_TYPE' orchestrator/src/config/profile.ts \ || fail "acceptance: the default obfs type must be declared in orchestrator/src/config/profile.ts" local default_declarations default_declarations="$(grep -rl 'DEFAULT_HYSTERIA_OBFS_TYPE\s*[:=]' orchestrator/src \ | grep -v 'orchestrator/src/config/profile.ts' || true)" [ -z "$default_declarations" ] \ || fail "acceptance: the default obfs type must not be re-declared in: $default_declarations" log_step "Acceptance: runtime env is derived from config, not literals" ! grep -Eq 'HY2XS_HYSTERIA_OBFS_TYPE=(gecko|salamander)' orchestrator/src/config/env.ts \ || fail "acceptance: renderRuntimeEnv must not print a hardcoded obfs type" grep -q 'HY2XS_HYSTERIA_OBFS_TYPE=\${config.hysteriaObfsType}' orchestrator/src/config/env.ts \ || fail "acceptance: renderRuntimeEnv must derive the obfs type from the parsed config" ! grep -Eq '\|\|\s*"(gecko|salamander)"' orchestrator/src/config/env.ts \ || fail "acceptance: env.ts must not carry its own obfs fallback default" log_step "Acceptance: package metadata records how the Hysteria version was chosen" grep -q '^hysteria_resolution=' "$package_dir/metadata/package.env" \ || fail "acceptance: metadata must record hysteria_resolution" grep -q '^hysteria_resolved_at=' "$package_dir/metadata/package.env" \ || fail "acceptance: metadata must record hysteria_resolved_at" grep -q '^hysteria_compat_gate=true$' "$package_dir/metadata/package.env" \ || fail "acceptance: release packages must be built with the Hysteria compatibility gate enabled" grep -Eq '^hysteria_artifact_url=https://github\.com/HyNetworks/hysteria/' "$package_dir/metadata/package.env" \ || fail "acceptance: Hysteria artifact must come from the canonical HyNetworks upstream" log_step "Acceptance: install-time never resolves a moving latest" ! grep -rq 'api.github.com' orchestrator/src/commands orchestrator/src/steps \ || fail "acceptance: install-time code must not query the upstream release API" ! grep -rq 'download.hysteria.network' orchestrator/src \ || fail "acceptance: install-time code must not use the moving latest download URL" log_step "Acceptance: smoke verifies config semantics, not substrings" grep -q 'assertHysteriaConfigMatchesProfile' orchestrator/src/steps/smoke.ts \ || fail "acceptance: smoke must verify the effective config semantically" log_step "Acceptance: admin export preserves unknown upstream fields and strips secrets" grep -q 'ExportHysteria2ConfigYaml' apps/controller/config.go \ || fail "acceptance: hysteria config export must go through the sanitizing raw-YAML path" grep -q 'GetRawHysteria2Config' apps/service/hysteria2_export.go \ || fail "acceptance: export must read the raw YAML instead of the typed model" log_step "Acceptance: frontend ACME diagnostics preserve upstream provider names" # Этот гейт проверял НЕ ТО, и его предмет исчез вместе с прежней страницей. # # Раньше он требовал, чтобы `apps/frontend/src/views/hysteria/list/index.vue` # содержал жёстко заданный список ACME DNS-провайдеров # # cloudflare duckdns gandi godaddy namecheap njalla porkbun vultr # # и не содержал `namedotcom`, удалённый upstream в Hysteria 2.11.0. Это имело # смысл ровно до тех пор, пока панель ПРЕДЛАГАЛА оператору выбор провайдера: # список в UI был вторым экземпляром upstream-реестра и мог от него отстать. # # Панель больше ничего не предлагает: конфигом владеет оркестратор, маршрутов # записи в API нет, а страница Hysteria стала read-only диагностикой. Имя # провайдера читается из фактического конфига и показывается как есть, поэтому # реестра в UI нет и быть не должно — новый upstream-провайдер отображается # без единой правки панели. # # Возврат старого списка ради прохождения этого grep'а создал бы фиктивный # реестр, существующий только для гейта. Поэтому проверяется действующий # контракт: провайдер приходит из профиля строкой и рисуется как значение. local hysteria_page="apps/frontend/src/views/hysteria/list/index.vue" local hysteria_types="apps/frontend/src/api/config/types.ts" code_has "$hysteria_page" -F -- 'text(profile.acme.dnsProvider)' \ || fail "acceptance: the panel must render the ACME DNS provider reported by the backend" code_has "$hysteria_types" -E -- 'dnsProvider:[[:space:]]*string[[:space:]]*\|[[:space:]]*null' \ || fail "acceptance: the ACME profile must carry the provider name as a generic string" # Значения параметров DNS-провайдера — ключ от DNS-зоны, и read-only экрану # они не нужны: показываются только имена. code_has "$hysteria_types" -F -- 'dnsConfigKeys: string[]' \ || fail "acceptance: the ACME DNS parameters must be exposed as key names only" # Селектор провайдера означал бы возврат к «панель владеет конфигом». ! code_has "$hysteria_page" -F -- ' build) throw new Error("проверка типов идёт после сборки bundle"); ' || fail "acceptance: проверка типов обязана предшествовать сборке bundle" log_step "Acceptance: local SVG icons are rendered from an in-repo sprite" # vite-plugin-svg-icons убран: он не обновлялся с 2022 года и тянул svgo 2.8, # postcss 5.2.18 и image-size 0.5.5, у последней advisory прямо сообщает # `Patched versions: <0.0.0` — исправленной версии не существует. ! grep -q '"vite-plugin-svg-icons"' apps/frontend/package.json \ || fail "acceptance: неподдерживаемый vite-plugin-svg-icons вернулся в зависимости" grep -q 'registerSvgIcons' apps/frontend/src/main.ts \ || fail "acceptance: спрайт локальных иконок не регистрируется при старте приложения" # Литерального скана по `virtual:svg-icons-register` здесь СОЗНАТЕЛЬНО нет. # # Он ловил не код, а прозу: единственное вхождение строки в дереве — в # блочном комментарии `components/SvgIcon/sprite.ts`, который объясняет, что # именно заменено. Комментарий, называющий удалённую вещь по имени, обязан # существовать — иначе он бесполезен, — и гейт, падающий на нём, запрещает # документировать собственную работу. # # Утверждение при этом не потеряно, а обеспечено более сильной проверкой: # production `vite build` выше по сборке уже прошёл. Активный # `import "virtual:svg-icons-register"` при отсутствующем плагине не # разрешается резолвером, и сборка bundle падает — то есть проверяется # исполняемый импорт, а не совпадение подстроки. # Контракт ассета проверяется ОБЩЕЙ функцией, а не второй её копией здесь. # # Раньше в этом месте лежала самостоятельная реализация проверки viewBox — # построчный двойник кода из sprite.ts. Пока проверялась одна вещь, это # выглядело безобидно; с добавлением проверки цвета две копии правил # разошлись бы так же, как разошлись две копии правила имени пира. Теперь # findIconContractViolations живёт рядом с кодом, который строит symbol, и # зовётся отсюда. # # Проверяется: система координат (без viewBox `` рисует иконку в # натуральную величину и обрезает её), отсутствие литеральных цветов у # монохромных иконок, отсутствие инлайнового style и непустого