fix(installer): harden admin smoke and rollback cleanup
This commit is contained in:
@@ -25,7 +25,10 @@ import { persistFailureState, runRollbackStages } from "../src/lib/rollback";
|
||||
// Пути считаются от файла теста, а не от cwd: `bun test` запускается и из корня
|
||||
// репозитория (сборка), и из orchestrator/ (разработчик).
|
||||
function source(relativeToSrc: string): string {
|
||||
return readFileSync(join(import.meta.dir, "..", "src", relativeToSrc), "utf8");
|
||||
return readFileSync(
|
||||
join(import.meta.dir, "..", "src", relativeToSrc),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
describe("стадии отката независимы", () => {
|
||||
@@ -33,7 +36,7 @@ describe("стадии отката независимы", () => {
|
||||
const executed: string[] = [];
|
||||
const failures = await runRollbackStages([
|
||||
{ name: "firewall", run: async () => void executed.push("firewall") },
|
||||
{ name: "services", run: async () => void executed.push("services") }
|
||||
{ name: "services", run: async () => void executed.push("services") },
|
||||
]);
|
||||
|
||||
expect(executed).toEqual(["firewall", "services"]);
|
||||
@@ -49,10 +52,13 @@ describe("стадии отката независимы", () => {
|
||||
run: async () => {
|
||||
executed.push("firewall");
|
||||
throw new Error("nft: command failed");
|
||||
}
|
||||
},
|
||||
},
|
||||
{ name: "restore configuration", run: async () => void executed.push("restore") },
|
||||
{ name: "stop services", run: async () => void executed.push("stop") }
|
||||
{
|
||||
name: "restore configuration",
|
||||
run: async () => void executed.push("restore"),
|
||||
},
|
||||
{ name: "stop services", run: async () => void executed.push("stop") },
|
||||
]);
|
||||
|
||||
expect(executed).toEqual(["firewall", "restore", "stop"]);
|
||||
@@ -67,15 +73,15 @@ describe("стадии отката независимы", () => {
|
||||
name: "firewall",
|
||||
run: async () => {
|
||||
throw new Error("first");
|
||||
}
|
||||
},
|
||||
},
|
||||
{ name: "healthy", run: async () => undefined },
|
||||
{
|
||||
name: "restore configuration",
|
||||
run: async () => {
|
||||
throw new Error("second");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(failures).toHaveLength(2);
|
||||
@@ -91,14 +97,14 @@ describe("стадии отката независимы", () => {
|
||||
name: "a",
|
||||
run: async () => {
|
||||
throw new Error("boom");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "b",
|
||||
run: async () => {
|
||||
throw new Error("boom");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(failures).toHaveLength(2);
|
||||
});
|
||||
@@ -109,8 +115,8 @@ describe("стадии отката независимы", () => {
|
||||
name: "weird",
|
||||
run: async () => {
|
||||
throw "строковая ошибка";
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(failures[0]).toContain("строковая ошибка");
|
||||
});
|
||||
@@ -135,7 +141,7 @@ describe("состояние отказа пишется best effort", () => {
|
||||
await expect(
|
||||
persistFailureState(async () => {
|
||||
throw new Error("ENOSPC: no space left on device");
|
||||
})
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -173,7 +179,10 @@ describe("install: откат обязателен после операцион
|
||||
// Прямых await-вызовов отката в теле rollbackFailedInstall быть не должно:
|
||||
// именно они и образовывали отменяемую цепочку.
|
||||
const start = installSource.indexOf("async function rollbackFailedInstall");
|
||||
const body = installSource.slice(start, installSource.indexOf("export async function install"));
|
||||
const body = installSource.slice(
|
||||
start,
|
||||
installSource.indexOf("export async function install"),
|
||||
);
|
||||
expect(body).toContain("await rollbackFirewallNow(context)");
|
||||
// Вызов существует только внутри стадии.
|
||||
const firewallAt = body.indexOf("await rollbackFirewallNow(context)");
|
||||
@@ -182,14 +191,19 @@ describe("install: откат обязателен после операцион
|
||||
});
|
||||
|
||||
test("остановка сервисов остаётся отдельными стадиями", () => {
|
||||
for (const stage of ["stop services", "disable services", "reset failed services"]) {
|
||||
for (const stage of [
|
||||
"stop services",
|
||||
"disable services",
|
||||
"reset failed hysteria-server",
|
||||
"reset failed hy2xs-admin",
|
||||
]) {
|
||||
expect(installSource).toContain(`name: "${stage}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test("чужие сервисы по-прежнему не трогаются", () => {
|
||||
expect(installSource).toContain(
|
||||
"systemd units were not deployed by this operation, leaving services untouched"
|
||||
"systemd units were not deployed by this operation, leaving services untouched",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -202,7 +216,7 @@ describe("reconfigure: откат обязателен после операци
|
||||
// выполняться ВНУТРИ persistFailureState, в какую бы строку его ни перенёс
|
||||
// редактор.
|
||||
const wrapped = reconfigureSource.match(
|
||||
/await persistFailureState\(\(\) =>\s*markPhase\(context,\s*classifyReconfigureFailure\(ownership, error\), message\)\s*\)/
|
||||
/await persistFailureState\(\(\) =>\s*markPhase\(context,\s*classifyReconfigureFailure\(ownership, error\), message\)\s*\)/,
|
||||
);
|
||||
expect(wrapped).not.toBeNull();
|
||||
});
|
||||
@@ -222,8 +236,12 @@ describe("reconfigure: откат обязателен после операци
|
||||
|
||||
// Регрессия: отказ rollbackFirewallNow отменял rollbackCurrentState целиком.
|
||||
test("порядок сохранён: сначала firewall, затем конфиги", () => {
|
||||
const firewall = reconfigureSource.indexOf("await rollbackFirewallNow(context)");
|
||||
const restore = reconfigureSource.indexOf("await rollbackCurrentState(context)");
|
||||
const firewall = reconfigureSource.indexOf(
|
||||
"await rollbackFirewallNow(context)",
|
||||
);
|
||||
const restore = reconfigureSource.indexOf(
|
||||
"await rollbackCurrentState(context)",
|
||||
);
|
||||
const stages = reconfigureSource.indexOf("await runRollbackStages(stages)");
|
||||
expect(firewall).toBeGreaterThan(-1);
|
||||
expect(firewall).toBeLessThan(restore);
|
||||
@@ -242,14 +260,19 @@ describe("reconfigure: откат обязателен после операци
|
||||
const start = reconfigureSource.indexOf("function restoreStages(");
|
||||
const body = reconfigureSource.slice(
|
||||
start,
|
||||
reconfigureSource.indexOf("async function rollbackCurrentState")
|
||||
reconfigureSource.indexOf("async function rollbackCurrentState"),
|
||||
);
|
||||
const muted = body
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.includes("runMutatingVisible`"))
|
||||
.filter((line) => line.includes("|| true") || line.includes("2>/dev/null"));
|
||||
.filter(
|
||||
(line) => line.includes("|| true") || line.includes("2>/dev/null"),
|
||||
);
|
||||
|
||||
expect(muted, `восстановление снова скрывает ошибки: ${muted.join("; ")}`).toEqual([]);
|
||||
expect(
|
||||
muted,
|
||||
`восстановление снова скрывает ошибки: ${muted.join("; ")}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test("восстановление разбито на независимые стадии", () => {
|
||||
@@ -260,7 +283,7 @@ describe("reconfigure: откат обязателен после операци
|
||||
"restore nftables files",
|
||||
"apply restored ruleset",
|
||||
"reload systemd units",
|
||||
"restart services"
|
||||
"restart services",
|
||||
]) {
|
||||
expect(reconfigureSource).toContain(`"${stage}"`);
|
||||
}
|
||||
@@ -269,7 +292,9 @@ describe("reconfigure: откат обязателен после операци
|
||||
// Неполное восстановление обязано доходить до итоговой сводки как отказ.
|
||||
test("частичное восстановление не выдаётся за успешное", () => {
|
||||
expect(reconfigureSource).toContain("restoreFailures.length > 0");
|
||||
expect(reconfigureSource).toContain("восстановление состояния выполнено не полностью");
|
||||
expect(reconfigureSource).toContain(
|
||||
"восстановление состояния выполнено не полностью",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,12 +307,18 @@ describe("install: команды отката не глушат собстве
|
||||
// них не выполнялось никогда.
|
||||
test("стадии остановки сервисов не подавляют код возврата", () => {
|
||||
const start = installSource.indexOf("async function rollbackFailedInstall");
|
||||
const body = installSource.slice(start, installSource.indexOf("export async function install"));
|
||||
const body = installSource.slice(
|
||||
start,
|
||||
installSource.indexOf("export async function install"),
|
||||
);
|
||||
const muted = body
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.includes("runMutatingVisible`"))
|
||||
.filter((line) => line.includes("|| true"));
|
||||
|
||||
expect(muted, `стадии отката снова скрывают ошибки: ${muted.join("; ")}`).toEqual([]);
|
||||
expect(
|
||||
muted,
|
||||
`стадии отката снова скрывают ошибки: ${muted.join("; ")}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user