/** * @file Tests for **Update 9router** on the AI Proxy page: hidden without a * 9router, a dialog that shows versions and starts the job, the server's * blocker (with the manual command) disabling the start, and the job's steps. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { NineRouterUpdate } from "../NineRouterUpdate"; import { api, type NineRouterUpdateJob, type NineRouterUpdateStatus } from "../../../lib/api"; vi.mock("../../../lib/api", () => ({ api: { aiProxies: { nineRouterUpdateStatus: vi.fn(), nineRouterUpdate: vi.fn() } }, })); vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), })); const statusMock = vi.mocked(api.aiProxies.nineRouterUpdateStatus); const startMock = vi.mocked(api.aiProxies.nineRouterUpdate); const base: NineRouterUpdateStatus = { installed: true, version: "0.5.104", latest: "0.5.109", updateAvailable: true, running: true, managedBy: "systemd", blocker: null, job: null, }; const job: NineRouterUpdateJob = { status: "running", startedAt: "2026-10-26T00:00:00Z", finishedAt: null, from: "0.5.104", to: null, managedBy: "systemd", install: "direct", steps: [ { id: "shutdown", status: "done", message: null }, { id: "update", status: "running", message: null }, { id: "relaunch", status: "pending", message: null }, ], log: "", }; beforeEach(() => { statusMock.mockReset(); startMock.mockReset(); }); describe("NineRouterUpdate", () => { it("renders nothing when 9router is not installed", async () => { statusMock.mockResolvedValue({ ...base, installed: false }); const { container } = render(); await waitFor(() => expect(statusMock).toHaveBeenCalled()); expect(container).toBeEmptyDOMElement(); }); it("opens with both versions and starts the three-step job", async () => { statusMock.mockResolvedValue(base); startMock.mockResolvedValue(job); render(); await userEvent.click(await screen.findByRole("button", { name: /aiProxy.update9router/ })); expect(statusMock).toHaveBeenLastCalledWith(true); const dialog = await screen.findByRole("dialog"); expect(dialog).toHaveTextContent("0.5.104"); expect(dialog).toHaveTextContent("0.5.109"); await userEvent.click(screen.getByRole("button", { name: "aiProxy.update9routerStart" })); expect(startMock).toHaveBeenCalledTimes(1); expect(await screen.findByText("aiProxy.update9routerStep.shutdown")).toBeInTheDocument(); expect(screen.getByText("aiProxy.update9routerStep.update")).toBeInTheDocument(); expect(screen.getByText("aiProxy.update9routerStep.relaunch")).toBeInTheDocument(); }); it("shows the blocker with its manual command and keeps the start disabled", async () => { statusMock.mockResolvedValue({ ...base, blocker: { code: "ENOPERM", message: "/usr/local is not writable by this account and sudo needs a password", command: "sudo npm install -g 9router@latest", }, }); render(); await userEvent.click(await screen.findByRole("button", { name: /aiProxy.update9router/ })); expect(await screen.findByText("sudo npm install -g 9router@latest")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "aiProxy.update9routerStart" })).toBeDisabled(); }); });