2b4a2cb2d5
Установщик мог повредить работающий сервер до того, как откажется его
трогать: install.sh переписывал /usr/local/lib/hy2xs, раскладывал
runtime-пакет и перезаписывал install-state.json, и только потом
запускал clean-host preflight. При ошибочном запуске поверх старой
установки rollback дополнительно делал stop и disable для работающих
hysteria-server и hy2xs-admin.
Установка разделена на две фазы с жёсткой границей:
PHASE 0 - read only: права, checksums пакета, clean-host preflight
из распакованного архива (новая команда preflight-install)
PHASE 1 - mutation: раскладка оркестратора и сама установка
Граница держится не соглашением, а read-only guard: под ним writeText,
writeTextAtomic и мутирующие раннеры lib/process кидают ошибку.
Внутри install() preflight выполняется раньше первой записи состояния.
Остальное в этом же инварианте:
- clean-host контракт расширен с двух маркеров до четырнадцати, пути
установки и данных берутся из конфигурации, а не захардкожены;
- отсутствие HY2XS_CONFIG_SCHEMA_VERSION трактуется как legacy, а не
как текущая схема: до v1 этого поля не существовало. Тест,
закреплявший прежнее поведение, инвертирован;
- install-state несёт идентификацию поколения (product, release_line,
config_schema_version); reconfigure и repair проверяют её до всего
остального, потому что installed: true мог остаться и от 0.x;
- repair требует явного --allow-partial-state;
- классификация отказа опирается на ownership-флаги, а не на текст
ошибки: раньше сообщение со словом nftables приводило к откату
чужого firewall. stop/disable выполняется только для юнитов,
развёрнутых текущей операцией, а fatal_pre_apply не делает
системного отката и не собирает diagnostics-бандл.
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
assertCleanHost,
|
|
detectLegacyMarkers,
|
|
legacyMarkersFor,
|
|
renderLegacyFailure,
|
|
type HostProbe
|
|
} from "../src/steps/cleanHost";
|
|
import { baselineConfig } from "./fixtures";
|
|
|
|
/**
|
|
* Проба, которая считает «существующими» ровно переданный набор целей.
|
|
* Это позволяет проверить контракт чистого хоста без файловой системы.
|
|
*/
|
|
function probeWith(present: readonly string[]): HostProbe {
|
|
const set = new Set(present);
|
|
return {
|
|
async fileExists(path) {
|
|
return set.has(path);
|
|
},
|
|
async dirExists(path) {
|
|
return set.has(path);
|
|
},
|
|
async unitExists(unit) {
|
|
return set.has(unit);
|
|
}
|
|
};
|
|
}
|
|
|
|
const config = baselineConfig();
|
|
|
|
describe("clean-host контракт", () => {
|
|
test("чистый хост проходит", async () => {
|
|
await expect(assertCleanHost(config, "bootstrap", probeWith([]))).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("каждый маркер по отдельности останавливает установку", async () => {
|
|
const markers = legacyMarkersFor(config, "bootstrap");
|
|
expect(markers.length).toBeGreaterThan(10);
|
|
|
|
for (const marker of markers) {
|
|
const probe = probeWith([marker.target]);
|
|
await expect(assertCleanHost(config, "bootstrap", probe)).rejects.toThrow(
|
|
/предыдущая или посторонняя установка/
|
|
);
|
|
}
|
|
});
|
|
|
|
test("список покрывает состояние, юниты, бинарник и наследие 0.x", () => {
|
|
const targets = legacyMarkersFor(config, "bootstrap").map((marker) => marker.target);
|
|
|
|
for (const expected of [
|
|
"/etc/hysteria/post-install.env",
|
|
"/etc/hy2xs/hy2xs.env",
|
|
"/var/lib/hy2xs/install-state.json",
|
|
"/etc/hy2xs/bootstrap-admin.secret",
|
|
"/usr/local/lib/hy2xs/package",
|
|
"/etc/hysteria/config.yaml",
|
|
"/usr/local/bin/hysteria",
|
|
"/etc/nftables.d/hy2xs.nft",
|
|
"hy2xs-admin.service",
|
|
"hysteria-server.service",
|
|
"h-ui.service",
|
|
"/usr/local/h-ui",
|
|
config.installDir,
|
|
config.dataDir
|
|
]) {
|
|
expect(targets).toContain(expected);
|
|
}
|
|
});
|
|
|
|
test("пути из конфигурации попадают в список, а не только дефолтные", () => {
|
|
const custom = baselineConfig({
|
|
HY2XS_INSTALL_DIR: "/srv/hy2xs-app",
|
|
HY2XS_DATA_DIR: "/srv/hy2xs-data"
|
|
});
|
|
const targets = legacyMarkersFor(custom, "bootstrap").map((marker) => marker.target);
|
|
expect(targets).toContain("/srv/hy2xs-app");
|
|
expect(targets).toContain("/srv/hy2xs-data");
|
|
});
|
|
|
|
// install.sh раскладывает runtime-пакет между фазами, поэтому в PHASE 1
|
|
// этот путь уже наш и маркером быть не может.
|
|
test("runtime-пакет — маркер в PHASE 0, но не в PHASE 1", async () => {
|
|
const probe = probeWith(["/usr/local/lib/hy2xs/package"]);
|
|
await expect(assertCleanHost(config, "bootstrap", probe)).rejects.toThrow(/usr\/local\/lib\/hy2xs\/package/);
|
|
await expect(assertCleanHost(config, "install", probe)).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("остальные маркеры продолжают работать и в PHASE 1", async () => {
|
|
const probe = probeWith(["hysteria-server.service"]);
|
|
await expect(assertCleanHost(config, "install", probe)).rejects.toThrow(/hysteria-server\.service/);
|
|
});
|
|
|
|
test("сообщение перечисляет все найденные маркеры и говорит, что хост не изменён", async () => {
|
|
const found = await detectLegacyMarkers(
|
|
legacyMarkersFor(config, "bootstrap"),
|
|
probeWith(["/etc/hy2xs/hy2xs.env", "hy2xs-admin.service"])
|
|
);
|
|
expect(found).toHaveLength(2);
|
|
|
|
const message = renderLegacyFailure(found);
|
|
expect(message).toContain("/etc/hy2xs/hy2xs.env");
|
|
expect(message).toContain("hy2xs-admin.service");
|
|
expect(message).toContain("Ни один файл на сервере не изменён");
|
|
expect(message).toContain("docs/14-legacy-cleanup.md");
|
|
});
|
|
});
|