import { describe, expect, test } from "bun:test"; import { assertCleanHost, detectLegacyMarkers, legacyMarkersFor, renderLegacyFailure, type HostProbe } from "../src/steps/cleanHost"; import { ORCHESTRATOR_INSTALL_DIR, ORCHESTRATOR_SYMLINK_PATH, RUNTIME_PACKAGE_DIR } from "../src/config/profile"; 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, probeWith([]))).resolves.toBeUndefined(); }); test("каждый маркер по отдельности останавливает установку", async () => { const markers = legacyMarkersFor(config); expect(markers.length).toBeGreaterThan(10); for (const marker of markers) { const probe = probeWith([marker.target]); await expect(assertCleanHost(config, probe)).rejects.toThrow( /предыдущая или посторонняя установка/ ); } }); test("список покрывает состояние, юниты, бинарник и наследие 0.x", () => { const targets = legacyMarkersFor(config).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", "/usr/local/lib/hy2xs", "/usr/local/bin/hy2xs-orchestrator", "/etc/hysteria/config.yaml", "/usr/local/bin/hysteria", "/var/lib/hysteria", "/etc/nftables.d/hy2xs.nft", "hy2xs-admin.service", "hysteria-server.service", "h-ui.service", "/usr/local/h-ui", config.installDir, config.dataDir, config.logDir ]) { expect(targets).toContain(expected); } }); /** * purge-v0.sh и clean-host обязаны описывать одну и ту же границу. * * Регрессия: purge удалял /var/lib/hysteria, /var/log/hy2xs, * /usr/local/lib/hy2xs и /usr/local/bin/hy2xs-orchestrator, а clean-host их * не проверял. Сервер, где остался только старый ACME-state Hysteria, * проходил проверку и получал свежую установку поверх чужого runtime. */ test("всё, что удаляет purge-v0.sh, проверяется clean-host контрактом", () => { const purgedPaths = [ "/etc/hy2xs", "/etc/hysteria", "/var/lib/hy2xs", "/var/lib/hy2xs-admin", "/var/lib/hysteria", "/var/log/hy2xs", "/opt/hy2xs-admin", "/usr/local/lib/hy2xs", "/usr/local/h-ui", "/usr/local/bin/hy2xs-orchestrator", "/usr/local/bin/hysteria", "/etc/nftables.d/hy2xs.nft" ]; const targets = legacyMarkersFor(config).map((marker) => marker.target); for (const purged of purgedPaths) { const covered = targets.some((target) => target === purged || target.startsWith(`${purged}/`)); expect(covered, `purge удаляет ${purged}, но clean-host его не проверяет`).toBe(true); } }); /** * Регрессия архитектуры установки. * * Пока PHASE 1 начиналась в install.sh, у списка маркеров была вторая, * «мягкая» версия: пути, которые shell успевал создать между фазами, * исключались из проверки. Это была не оптимизация, а компенсация того, что * мутировали двое. * * Теперь bootstrap выполняет сам оркестратор — уже ПОСЛЕ clean-host, — и * послаблений быть не должно: любой из этих путей на входе означает чужую * установку, при любом вызове. */ test("bootstrap-пути остаются маркерами без всяких исключений", async () => { for (const target of [ORCHESTRATOR_INSTALL_DIR, RUNTIME_PACKAGE_DIR, ORCHESTRATOR_SYMLINK_PATH]) { await expect(assertCleanHost(config, probeWith([target]))).rejects.toThrow( /предыдущая или посторонняя установка/ ); } }); test("маркеры bootstrap-путей берутся из профиля, а не из копий строк", () => { const targets = legacyMarkersFor(config).map((marker) => marker.target); expect(targets).toContain(ORCHESTRATOR_INSTALL_DIR); expect(targets).toContain(RUNTIME_PACKAGE_DIR); expect(targets).toContain(ORCHESTRATOR_SYMLINK_PATH); }); test("пути из конфигурации попадают в список, а не только дефолтные", () => { const custom = baselineConfig({ HY2XS_INSTALL_DIR: "/srv/hy2xs-app", HY2XS_DATA_DIR: "/srv/hy2xs-data" }); const targets = legacyMarkersFor(custom).map((marker) => marker.target); expect(targets).toContain("/srv/hy2xs-app"); expect(targets).toContain("/srv/hy2xs-data"); }); test("сообщение перечисляет все найденные маркеры и говорит, что хост не изменён", async () => { const found = await detectLegacyMarkers( legacyMarkersFor(config), 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/operations/14-legacy-cleanup.md"); }); });