// @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(), api: calls })); const runtime = vi.hoisted(() => ({ distributed: false, fallback: false, apiBase: "/api/v1", rootDomain: "example.test" })); vi.mock("@/lib/hooks", async (original) => ({ ...await original(), 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(); } 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(); }); });