import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; /** * Порядок фиксации успеха операции. * * Дефект, который здесь закрепляется, — commit ordering. Успешный install * заканчивался так: * * smoke PASS * -> cancelFirewallRollback() // снимала таймер И УДАЛЯЛА резервные копии * -> advanceInstallState("installed") * * То есть единственные данные для отката firewall (marker, прежний * nftables.conf, прежний hy2xs.nft) уничтожались ДО долговечной записи успеха. * Если эта запись падала — ENOSPC, EIO, read-only ФС, — управление уходило в * обработчик ошибки, обязательный откат честно запускался и сообщал: * * firewall rollback skipped: no HY2XS rollback markers found * * Откат нельзя было отменить, но откатывать ему было нечем. Причём падение * записи маркера — ровно тот сценарий, который был специально сделан * безопасным предыдущим патчем. * * Правильный порядок: снять таймер (копии остаются) -> зафиксировать успех * долговечно -> и только потом убрать копии, best-effort. */ function source(relativeToSrc: string): string { return readFileSync(join(import.meta.dir, "..", "src", relativeToSrc), "utf8"); } const firewallSource = source("steps/firewall.ts"); describe("снятие таймера и удаление копий — разные операции", () => { test("firewall предоставляет disarm и cleanup по отдельности", () => { expect(firewallSource).toContain("export async function disarmFirewallRollback"); expect(firewallSource).toContain("export async function cleanupFirewallRollback"); }); // Прежнее единое имя не должно вернуться: оно и означало «снять таймер и // удалить копии одним действием». test("объединённая cancelFirewallRollback не вернулась", () => { expect(firewallSource).not.toContain("export async function cancelFirewallRollback"); for (const file of ["commands/install.ts", "commands/reconfigure.ts"]) { expect(source(file)).not.toContain("cancelFirewallRollback"); } }); test("disarm не удаляет резервные копии", () => { const start = firewallSource.indexOf("export async function disarmFirewallRollback"); const body = firewallSource.slice(start, firewallSource.indexOf("export async function cleanupFirewallRollback")); expect(body).not.toContain("cleanupFirewallBackupFiles"); expect(body).toContain("stopRollbackTimer"); }); test("cleanup удаляет копии и ничего больше", () => { const start = firewallSource.indexOf("export async function cleanupFirewallRollback"); const body = firewallSource.slice(start, firewallSource.indexOf("export async function rollbackFirewallNow")); expect(body).toContain("cleanupFirewallBackupFiles"); }); }); describe("install: успех фиксируется до уничтожения данных отката", () => { const installSource = source("commands/install.ts"); test("порядок: disarm -> durable installed -> cleanup", () => { const disarm = installSource.indexOf("await disarmFirewallRollback(context)"); const installed = installSource.indexOf('await advanceInstallState(context, ownership, "installed")'); const cleanup = installSource.indexOf("await cleanupFirewallRollback(context)"); expect(disarm).toBeGreaterThan(-1); expect(installed).toBeGreaterThan(disarm); expect(cleanup).toBeGreaterThan(installed); }); test("успешный smoke фиксируется отдельной фазой до снятия таймера", () => { const smokeOk = installSource.indexOf('await advanceInstallState(context, ownership, "smoke_ok")'); const disarm = installSource.indexOf("await disarmFirewallRollback(context)"); expect(smokeOk).toBeGreaterThan(-1); expect(smokeOk).toBeLessThan(disarm); }); // Уборка после фиксации успеха — мусор в /run, а не причина объявить // успешную установку неуспешной. test("уборка после фиксации выполняется best-effort", () => { const cleanup = installSource.indexOf("await cleanupFirewallRollback(context)"); const tryAt = installSource.lastIndexOf("try {", cleanup); const catchAt = installSource.indexOf("catch (cleanupError)", cleanup); expect(tryAt).toBeGreaterThan(-1); expect(tryAt).toBeLessThan(cleanup); expect(catchAt).toBeGreaterThan(cleanup); }); }); describe("reconfigure: тот же порядок фиксации", () => { const reconfigureSource = source("commands/reconfigure.ts"); test("порядок: disarm -> durable installed -> cleanup", () => { const disarm = reconfigureSource.indexOf("await disarmFirewallRollback(context)"); const installed = reconfigureSource.indexOf('await markPhase(context, "installed")'); const cleanup = reconfigureSource.indexOf("cleanupFirewallRollback(context)"); expect(disarm).toBeGreaterThan(-1); expect(installed).toBeGreaterThan(disarm); expect(cleanup).toBeGreaterThan(installed); }); test("уборка после фиксации выполняется best-effort", () => { expect(reconfigureSource).toContain("catch (cleanupError)"); const cleanup = reconfigureSource.indexOf("cleanupFirewallRollback(context)"); const catchAt = reconfigureSource.indexOf("catch (cleanupError)", cleanup); expect(catchAt).toBeGreaterThan(cleanup); }); });