fix(installer): harden admin smoke and rollback cleanup

This commit is contained in:
2026-09-07 22:39:30 +05:00
parent bf10810cfc
commit 079094591b
15 changed files with 1095 additions and 261 deletions
+46 -9
View File
@@ -1059,24 +1059,48 @@ EOF
|| fail "acceptance: smoke больше не выполняет настоящий вход в панель"
code_has orchestrator/src/steps/smoke.ts -F -- 'ADMIN_LOGIN_PATH' \
|| fail "acceptance: адрес формы входа перестал браться из профиля"
# Успех определяется КОНВЕРТОМ: админка отвечает 200 OK и на отказ тоже.
code_has orchestrator/src/steps/smoke.ts -F -- '"code"\s*:\s*20000' \
# Успех определяется разобранным КОНВЕРТОМ: админка отвечает 200 OK и на
# отказ тоже, а regex по сырому тексту способен принять подстроку в message.
code_has orchestrator/src/steps/smoke.ts -F -- 'envelope?.code === 20000' \
|| fail "acceptance: успех входа снова определяется кодом HTTP, а не конвертом ответа"
# Команда несёт пароль администратора и не имеет права попасть в текст ошибки:
# runReadOnly (без Secret) кладёт её туда, а текст уезжает в журнал установки
# и в diagnostics-бандл. Проверяется ТЕЛО функции, а не файл: остальной smoke
# законно пользуется обоими раннерами.
local login_body
# Positive и negative probe обязаны пользоваться одной транспортной функцией:
# две независимые curl-команды и разошлись в RC3 с production middleware.
local login_body login_request_body
login_body="$(sed -n '/^async function assertAdminLoginWorks/,/^}/p' orchestrator/src/steps/smoke.ts)" \
|| fail "acceptance: не удалось прочитать проверку входа в панель"
[ -n "$login_body" ] \
|| fail "acceptance: функция проверки входа в панель не найдена"
grep -q 'runReadOnlySecret`curl' <<<"$login_body" \
[ "$(grep -c 'requestAdminLogin(' <<<"$login_body")" -eq 2 ] \
|| fail "acceptance: positive и negative login probe не используют один request helper"
! grep -q 'curl ' <<<"$login_body" \
|| fail "acceptance: assertAdminLoginWorks снова содержит отдельную curl-команду"
login_request_body="$(sed -n '/^async function requestAdminLogin/,/^}/p' orchestrator/src/steps/smoke.ts)" \
|| fail "acceptance: не удалось прочитать transport helper входа"
[ -n "$login_request_body" ] \
|| fail "acceptance: общий transport helper входа не найден"
grep -q 'runReadOnlySecret`curl' <<<"$login_request_body" \
|| fail "acceptance: запрос входа не выполняется секретным раннером"
# `runReadOnly`curl` совпадает только с ГОЛОЙ формой: у секретной после
# `runReadOnly` идёт `Secret`, а не обратная кавычка.
! grep -q 'runReadOnly`curl' <<<"$login_body" \
! grep -q 'runReadOnly`curl' <<<"$login_request_body" \
|| fail "acceptance: запрос входа выполняется раннером, кладущим команду в текст ошибки"
grep -q -- '--user-agent' <<<"$login_request_body" \
|| fail "acceptance: login smoke снова наследует блокируемый curl User-Agent"
# Wire-contract проверяется значением, а не поиском форматирования в исходнике.
"$BUN_BIN" -e '
const smoke = await import("./orchestrator/src/steps/smoke.ts");
const request = smoke.buildAdminLoginRequest("hy2xsadmin", "quote\" slash\\ tail ");
const body = JSON.parse(request.body);
if (body.username !== "hy2xsadmin" || body.pass !== "quote\" slash\\ tail ") {
throw new Error("login helper искажает username/pass");
}
if (Object.hasOwn(body, "password")) throw new Error("login helper отправляет alias password");
if (!request.userAgent || /fofa|shodan|curl|wget/i.test(request.userAgent)) {
throw new Error("login helper отправляет scanner-like User-Agent");
}
' || fail "acceptance: общий login helper нарушает wire-контракт или scanner middleware"
log_step "Acceptance: the negative login probe proves a rejection"
# Проверка `code.trim() === "200"` доказывала ровно одно: что запрос не
@@ -1084,6 +1108,8 @@ EOF
# поэтому «неверные учётные данные отвергнуты» из неё не следовало.
code_has orchestrator/src/steps/smoke.ts -F -- 'isRejectedLogin' \
|| fail "acceptance: отрицательная проба снова определяет отказ по коду HTTP"
code_has orchestrator/src/steps/smoke.ts -F -- 'JSON.parse(body)' \
|| fail "acceptance: login envelope снова проверяется regex по сырому тексту"
code_has orchestrator/src/steps/smoke.ts -F -- 'INVALID_CREDENTIALS_CODE = "invalid_credentials"' \
|| fail "acceptance: отрицательная проба не сверяет доменный код отказа"
# Литерал в исходнике теоретически может оказаться настоящим паролем — тогда
@@ -1579,6 +1605,17 @@ run_single_owner_acceptance() {
grep -q 'persistFailureState(' "$rollback_command" \
|| fail "acceptance: запись состояния отказа в $rollback_command не помечена как best effort"
done
# reset-failed — cleanup с допустимым исходом «юнит уже выгружен». Ненулевой
# код нельзя ни превращать в ложный manual-recovery warning, ни глушить:
# решение принимается после наблюдения ActiveState каждого юнита.
grep -q 'resetFailedUnit("hysteria-server")' orchestrator/src/commands/install.ts \
|| fail "acceptance: rollback install не проверяет postcondition reset-failed для hysteria-server"
grep -q 'resetFailedUnit("hy2xs-admin")' orchestrator/src/commands/install.ts \
|| fail "acceptance: rollback install не проверяет postcondition reset-failed для hy2xs-admin"
code_has orchestrator/src/lib/systemd.ts -F -- 'runMutatingStatus`systemctl reset-failed' \
|| fail "acceptance: reset-failed снова бросает до проверки допустимого not-loaded состояния"
code_has orchestrator/src/lib/systemd.ts -F -- 'properties.get("ActiveState")' \
|| fail "acceptance: успех reset-failed не доказывается наблюдаемым ActiveState"
"$BUN_BIN" -e '
const fs = require("node:fs");
for (const [file, write] of [