import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { api, type ConfigurationCatalog, type ConfigurationPreview } from "../lib/api"; import { ConfigurationEditor } from "./ConfigurationEditor"; const catalog: ConfigurationCatalog = { environment: "test", read_only: false, revision: 0, overrides: {}, settings: [{ key: "gateway_max_batch_size", display_name: "Gateway batch limit", description: "Inputs per request", environment_variable: "MODELFORGE_GATEWAY_MAX_BATCH_SIZE", sensitivity: "public", scope: "control_plane", required_in_production: false, application_mode: "dynamic", editable: true, configured: true, source: "pydantic_default", value: 8, category: "gateway", data_type: "integer", default_value: 8, validation_rules: { minimum: 1, maximum: 128 }, consumer: "control_plane.serving", rollback: "Preview a prior revision", }], }; const preview: ConfigurationPreview = { expected_revision: 0, preview_digest: "a".repeat(64), overrides: { gateway_max_batch_size: 2 }, differences: [{ key: "gateway_max_batch_size", before: 8, after: 2, source_before: "pydantic_default", source_after: "operator_override" }], affected_components: ["control_plane.serving"], impact: "New requests only; no restart.", rollback: "Preview a prior revision.", }; afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe("Configuration changes", () => { it("allows a viewer to inspect history but disables configuration proposals", () => { render(); expect(screen.getByLabelText(/Gateway batch limit/)).toBeDisabled(); expect(screen.getByRole("button", { name: "Preview import" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Load revision history" })).not.toBeDisabled(); expect(screen.getByRole("status")).toHaveTextContent("Viewer access"); }); it("allows operator preview without offering administrator apply", async () => { vi.spyOn(api, "configurationPreview").mockResolvedValue(preview); render(); fireEvent.change(screen.getByLabelText(/Gateway batch limit/), { target: { value: "2" } }); fireEvent.click(screen.getByRole("button", { name: "Preview changes" })); await screen.findByText("Review proposed revision"); expect(screen.getByLabelText("I reviewed the impact and authorize this change.")).toBeDisabled(); expect(screen.getByRole("button", { name: "Apply reviewed changes" })).toBeDisabled(); }); it("explains field bounds and the queue timeout dependency beside the control", () => { const queueTimeout = { ...catalog.settings[0], key: "gateway_queue_timeout_seconds", display_name: "Gateway queue timeout", value: 30, validation_rules: { minimum: 1, maximum: 900, less_than_setting: "gateway_request_timeout_seconds" }, }; const requestTimeout = { ...catalog.settings[0], key: "gateway_request_timeout_seconds", display_name: "Gateway request timeout", value: 45, validation_rules: { minimum: 5, maximum: 900 }, }; render(); const input = screen.getByLabelText(/Gateway queue timeout/); expect(input).toHaveAttribute("aria-describedby", "validation-editor-gateway_queue_timeout_seconds"); expect(screen.getByText("Allowed: at least 1 and at most 900. Must be less than Gateway request timeout.")) .toBeInTheDocument(); }); it("requires preview and explicit confirmation before applying the reviewed values", async () => { const proposed = vi.spyOn(api, "configurationPreview").mockResolvedValue(preview); const apply = vi.spyOn(api, "configurationApply").mockResolvedValue({ revision: 1, status: "accepted_for_new_requests", acknowledged_by: "control_plane.serving", preview_digest: preview.preview_digest }); const refreshed = vi.fn().mockResolvedValue(undefined); render(); expect(screen.getByRole("button", { name: "Preview changes" })).toBeDisabled(); fireEvent.change(screen.getByLabelText(/Gateway batch limit/), { target: { value: "2" } }); fireEvent.click(screen.getByRole("button", { name: "Preview changes" })); expect(await screen.findByText("Review proposed revision")).toBeInTheDocument(); expect(screen.getByText("Pydantic default → Operator override")).toBeInTheDocument(); expect(proposed).toHaveBeenCalledWith({ expected_revision: 0, changes: { gateway_max_batch_size: 2 } }); expect(apply).not.toHaveBeenCalled(); expect(screen.getByRole("button", { name: "Apply reviewed changes" })).toBeDisabled(); fireEvent.click(screen.getByLabelText("I reviewed the impact and authorize this change.")); fireEvent.click(screen.getByRole("button", { name: "Apply reviewed changes" })); expect(await screen.findByText(/Revision 1 accepted for new serving requests/)).toBeInTheDocument(); expect(refreshed).toHaveBeenCalledOnce(); }); it("discards a preview after edits and after an external revision changes", async () => { vi.spyOn(api, "configurationPreview").mockResolvedValue(preview); const { rerender } = render(); fireEvent.change(screen.getByLabelText(/Gateway batch limit/), { target: { value: "2" } }); fireEvent.click(screen.getByRole("button", { name: "Preview changes" })); await screen.findByText("Review proposed revision"); fireEvent.change(screen.getByLabelText(/Gateway batch limit/), { target: { value: "3" } }); expect(screen.queryByText("Review proposed revision")).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Preview changes" })); await screen.findByText("Review proposed revision"); rerender(); expect(screen.queryByText("Review proposed revision")).not.toBeInTheDocument(); }); it("reports conflicts without reporting application or retaining confirmation", async () => { vi.spyOn(api, "configurationPreview").mockResolvedValue(preview); vi.spyOn(api, "configurationApply").mockRejectedValue(new Error("Configuration changed")); render(); fireEvent.change(screen.getByLabelText(/Gateway batch limit/), { target: { value: "2" } }); fireEvent.click(screen.getByRole("button", { name: "Preview changes" })); await screen.findByText("Review proposed revision"); fireEvent.click(screen.getByLabelText("I reviewed the impact and authorize this change.")); fireEvent.click(screen.getByRole("button", { name: "Apply reviewed changes" })); expect(await screen.findByRole("alert")).toHaveTextContent("Configuration changed"); expect(screen.queryByText(/accepted for new serving requests/)).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Apply reviewed changes" })).not.toBeInTheDocument(); }); it("previews import and rollback without automatically applying either", async () => { const change = { expected_revision: 0, changes: { gateway_max_batch_size: 2 } }; const imported = vi.spyOn(api, "configurationImportPreview").mockResolvedValue({ change, preview }); const history = vi.spyOn(api, "configurationHistory").mockResolvedValue([]); const apply = vi.spyOn(api, "configurationApply"); render(); fireEvent.click(screen.getByText("Export and import safe overrides")); fireEvent.change(screen.getByLabelText("Configuration import"), { target: { value: "bad JSON" } }); fireEvent.click(screen.getByRole("button", { name: "Preview import" })); expect(await screen.findByRole("alert")).toHaveTextContent("valid JSON"); expect(imported).not.toHaveBeenCalled(); fireEvent.change(screen.getByLabelText("Configuration import"), { target: { value: "{}" } }); fireEvent.click(screen.getByRole("button", { name: "Preview import" })); await screen.findByText("Review proposed revision"); expect(apply).not.toHaveBeenCalled(); fireEvent.click(screen.getByText("Revision history and rollback")); fireEvent.click(screen.getByRole("button", { name: "Load revision history" })); await waitFor(() => expect(history).toHaveBeenCalledOnce()); expect(await screen.findByText("No operator revisions yet.")).toBeInTheDocument(); }); });