569b68ec98
Settings → AI Proxy gains an admin-only Update 9router button beside Config 9router. One in-memory job (server/lib/nine-router-update.js) stops 9router (its user unit, else this account's own 9router processes), runs npm install -g --prefix <its prefix> 9router@latest (sudo -n only when sudoers already allows it, probed before anything stops), then relaunches it hidden only if it was running. A failed update still relaunches the old version. Nothing in the request names a command, path or package.
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
/**
|
|
* @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ĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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(<NineRouterUpdate />);
|
|
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(<NineRouterUpdate />);
|
|
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(<NineRouterUpdate />);
|
|
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();
|
|
});
|
|
});
|