fix(panel): diagnose runtime failures and add owner recovery controls
This commit is contained in:
47 files changed
+1590
-102
No files matched your search
@@ -0,0 +1,86 @@
|
||||
import { test, expect, t, expectFitsScreen } from "./fixtures";
|
||||
|
||||
test("platform policy is an Owner entry and persists after reload", async ({ page, signIn }) => {
|
||||
await signIn("owner");
|
||||
await page.goto("/admin/platform");
|
||||
await expect(page.getByRole("heading", { name: t("admin:platform_title"), exact: true })).toBeVisible();
|
||||
const limit = page.getByLabel(t("admin:platform_limit"));
|
||||
await expect(limit).toBeEnabled();
|
||||
await limit.fill("3");
|
||||
await page.getByLabel(t("admin:platform_cooldown")).fill("45");
|
||||
await page.getByRole("button", { name: t("admin:platform_save") }).click();
|
||||
await expect(page.getByText(t("admin:platform_saved"), { exact: true })).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(limit).toHaveValue("3");
|
||||
await expect(page.getByLabel(t("admin:platform_cooldown"))).toHaveValue("45");
|
||||
await expectFitsScreen(page);
|
||||
await signIn("linked");
|
||||
await page.goto("/admin/platform");
|
||||
await expect(page.getByRole("heading", { name: t("admin:platform_title"), exact: true })).toBeHidden();
|
||||
expect((await page.request.get("/api/v1/settings/wake-policy")).status()).toBe(403);
|
||||
});
|
||||
|
||||
test("source disclosure does not scale on press and long pages keep bottom breathing room", async ({ page, signIn }) => {
|
||||
await signIn("owner");
|
||||
for (const viewport of [{ width: 1440, height: 600 }, { width: 375, height: 812 }]) {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/admin/auth-sources");
|
||||
const disclosure = page.getByRole("button", { name: t("authSources:expand", { name: "littleskin" }) });
|
||||
const box = await disclosure.boundingBox();
|
||||
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
|
||||
await page.mouse.down();
|
||||
await expect.poll(() => disclosure.evaluate((el) => getComputedStyle(el).transform)).toMatch(/^(none|matrix\(1, 0, 0, 1, 0, 0\))$/);
|
||||
await page.mouse.up();
|
||||
await expect(page.getByRole("button", { name: t("authSources:collapse", { name: "littleskin" }) })).toHaveAttribute("aria-expanded", "true");
|
||||
const main = page.getByRole("main");
|
||||
await main.evaluate((el) => { el.scrollTop = el.scrollHeight; });
|
||||
const save = page.getByRole("button", { name: t("authSources:save"), exact: true });
|
||||
await expect(save).toBeInViewport();
|
||||
const room = await save.evaluate((el) => el.closest("main")!.getBoundingClientRect().bottom - el.parentElement!.parentElement!.getBoundingClientRect().bottom);
|
||||
expect(room).toBeGreaterThanOrEqual(23);
|
||||
await expectFitsScreen(page);
|
||||
}
|
||||
});
|
||||
|
||||
test("startup shows actual scheduling blockage, stale API state, then recovery", async ({ page, signIn }) => {
|
||||
await signIn("owner");
|
||||
let state: "blocked" | "outage" | "running" = "blocked";
|
||||
await page.route("**/api/v1/servers/survival/status", async (route) => {
|
||||
if (state === "outage") { await route.fulfill({ status: 503, json: { error: { code: "upstream_unavailable", message: "test API outage" } } }); return; }
|
||||
const original = await route.fetch();
|
||||
const data = await original.json();
|
||||
await route.fulfill({ json: { ...data, phase: state === "running" ? "Running" : "Starting", desiredState: "Running", ready: state === "running", startup: state === "blocked" ? { stage: "scheduling", reason: "Unschedulable", message: "0/1 nodes: Insufficient memory", startedAt: new Date(Date.now() - 60000).toISOString(), logsAvailable: false } : undefined } });
|
||||
});
|
||||
await page.goto("/servers/survival");
|
||||
await expect(page.getByText(t("servers:startup_scheduling"), { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(t("servers:startup_memory"), { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(t("servers:log_ended"), { exact: true })).toBeHidden();
|
||||
await expect(page.getByRole("button", { name: t("servers:retire_delete"), exact: true })).toBeHidden();
|
||||
state = "outage";
|
||||
await expect(page.getByText(t("servers:status_stale"), { exact: true })).toBeVisible({ timeout: 7000 });
|
||||
await expect(page.getByText(t("servers:startup_scheduling"), { exact: true })).toBeHidden();
|
||||
await expect(page.getByRole("button", { name: t("servers:stop"), exact: true })).toBeHidden();
|
||||
state = "running";
|
||||
await expect(page.getByText(t("servers:status_stale"), { exact: true })).toBeHidden({ timeout: 7000 });
|
||||
await page.getByRole("button", { name: t("servers:more_actions"), exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: t("servers:retire_delete"), exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Owner emergency stop stays secondary, confirms name, and distinguishes acceptance from shutdown", async ({ page, signIn }) => {
|
||||
await signIn("owner");
|
||||
await page.goto("/servers/survival");
|
||||
await page.getByRole("button", { name: t("servers:more_actions"), exact: true }).click();
|
||||
await page.getByRole("button", { name: t("servers:emergency_stop"), exact: true }).click();
|
||||
const confirm = page.getByRole("button", { name: t("servers:emergency_stop"), exact: true });
|
||||
await expect(confirm).toBeDisabled();
|
||||
await page.getByLabel(t("servers:retire_confirm_label"), { exact: false }).fill("survival");
|
||||
await confirm.click();
|
||||
await expect(page.getByText(t("servers:emergency_stop_accepted"), { exact: true })).toBeVisible();
|
||||
await expect(confirm).toBeDisabled();
|
||||
await page.getByRole("dialog").getByRole("button", { name: t("common:close_sr"), exact: true }).last().click();
|
||||
await expectFitsScreen(page);
|
||||
await page.goto("/admin/platform");
|
||||
const ownerNav = page.getByRole("navigation").getByRole("link");
|
||||
const links = await ownerNav.evaluateAll((elements) => elements.map((el) => el.getAttribute("href")));
|
||||
expect(links.at(-1)).toBe("/admin/platform");
|
||||
});
|
||||
Reference in new issue
Block a user