Files
Felis/panel/src/pages/admin/PlatformSettingsPage.test.tsx
T

88 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { PlatformSettingsPage } from "./PlatformSettingsPage";
import type { WakePolicySettings } from "@/lib/types";
const calls = vi.hoisted(() => ({ getEntryPolicy: vi.fn(), setEntryPolicy: vi.fn(), fleet: vi.fn(), getWakePolicy: vi.fn(), setWakePolicy: vi.fn(), nodes: vi.fn(), nodeTasks: vi.fn(), nodeTask: vi.fn(), startNodeTask: vi.fn(), retryNodeTask: vi.fn() }));
vi.mock("@/lib/api", async (original) => ({ ...await original<typeof import("@/lib/api")>(), api: calls }));
const runtime = vi.hoisted(() => ({ distributed: false, fallback: false, apiBase: "/api/v1", rootDomain: "example.test" }));
vi.mock("@/lib/hooks", async (original) => ({ ...await original<typeof import("@/lib/hooks")>(), useConfig: () => runtime }));
const policy: WakePolicySettings = { maxRunningServers: 0, wakeCooldownSeconds: 0, revision: "initial", managed: false };
const limit = () => screen.getByRole("spinbutton", { name: "Running server limit (0–10000)" }) as HTMLInputElement;
const cooldown = () => screen.getByRole("spinbutton", { name: "Wake cooldown (0–3600 seconds)" });
function page() { render(<MemoryRouter><PlatformSettingsPage /></MemoryRouter>); }
beforeEach(() => {
vi.clearAllMocks();
runtime.distributed = false;
runtime.fallback = false;
calls.getEntryPolicy.mockResolvedValue({ mode: "domain", defaultServer: "", requireAccountLink: true, offlineAction: "wake", waitingSpace: "lobby", fallbackServer: "", revision: "initial" });
calls.fleet.mockResolvedValue([]);
calls.nodes.mockResolvedValue([]);
calls.nodeTasks.mockResolvedValue({ available: false, tasks: [] });
calls.getWakePolicy.mockResolvedValue(policy);
calls.setWakePolicy.mockImplementation(async (body) => ({ ...body, revision: "saved", managed: true }));
});
describe("platform policy", () => {
it("keeps the form visible during loading without offering placeholder values as saved configuration", async () => {
let resolve!: (policy: WakePolicySettings) => void;
calls.getWakePolicy.mockReturnValue(new Promise((r) => { resolve = r; }));
page();
expect(limit().disabled).toBe(true);
expect(limit().value).toBe("");
expect(screen.getAllByRole("status").some((node) => node.textContent?.includes("Loading the saved policy"))).toBe(true);
await act(async () => resolve(policy));
expect(limit().disabled).toBe(false);
expect(limit().value).toBe("0");
});
it("saves validated values with the read revision and preserves drafts on a conflict", async () => {
page(); await waitFor(() => expect(limit().disabled).toBe(false));
fireEvent.change(limit(), { target: { value: "2" } });
fireEvent.change(cooldown(), { target: { value: "30" } });
calls.setWakePolicy.mockRejectedValueOnce({ status: 409, code: "conflict", message: "changed" });
await userEvent.click(screen.getByRole("button", { name: "Save and apply" }));
await screen.findByRole("alert");
expect(limit().value).toBe("2");
expect(screen.getAllByRole("button", { name: "Reload" })[0]).toHaveProperty("disabled", true);
await userEvent.click(screen.getByRole("button", { name: "Save and apply" }));
await screen.findByText("Saved. The new policy is active without a restart.");
expect(calls.setWakePolicy).toHaveBeenLastCalledWith({ maxRunningServers: 2, wakeCooldownSeconds: 30, revision: "initial" });
fireEvent.change(limit(), { target: { value: "1.5" } });
expect(screen.getByRole("button", { name: "Save and apply" })).toHaveProperty("disabled", true);
await userEvent.click(screen.getByRole("button", { name: "Discard changes" }));
expect(limit().value).toBe("2");
});
});
describe("distributed deployment entry", () => {
it("explains single-node deployment and enrollment without requesting unavailable nodes", async () => {
page();
await waitFor(() => expect(limit().disabled).toBe(false));
expect(screen.getByText("Single-node mode")).toBeTruthy();
expect(screen.getByText("Node operations")).toBeTruthy();
expect(await screen.findByText(/Host node service is not installed/)).toBeTruthy();
expect(calls.nodes).not.toHaveBeenCalled();
});
it("shows existing execution nodes and links to server placement and migration", async () => {
runtime.distributed = true;
calls.nodes.mockResolvedValue([{ name: "worker-b", role: "worker", ready: true, approved: false, architecture: "arm64", addresses: ["192.0.2.11"] }]);
page();
expect(await screen.findByText("worker-b")).toBeTruthy();
expect(screen.getByText("Distributed mode enabled")).toBeTruthy();
expect(screen.getByText("Pending approval")).toBeTruthy();
expect(screen.getByRole("link", { name: "Manage server placement and migration" }).getAttribute("href")).toBe("/servers");
});
it("keeps a failed config read distinct from single-node mode", async () => {
runtime.fallback = true;
runtime.distributed = true;
page();
await waitFor(() => expect(limit().disabled).toBe(false));
expect(screen.getByText("Deployment mode unconfirmed")).toBeTruthy();
expect(screen.queryByText("Single-node mode")).toBeNull();
expect(calls.nodes).not.toHaveBeenCalled();
});
});