Files
nanobot/webui/src/tests/api.test.ts
T

263 lines
7.6 KiB
TypeScript
Raw Normal View History

import { beforeEach, describe, expect, it, vi } from "vitest";
2026-05-06 15:54:15 +00:00
import {
deleteSession,
2026-05-22 22:25:12 +08:00
fetchCliApps,
fetchSidebarState,
fetchWebuiThread,
2026-05-06 15:54:15 +00:00
listSessions,
listSlashCommands,
2026-05-22 22:25:12 +08:00
runCliAppAction,
updateSidebarState,
updateImageGenerationSettings,
updateProviderSettings,
2026-05-06 15:54:15 +00:00
updateSettings,
2026-05-09 06:48:26 +00:00
updateWebSearchSettings,
2026-05-06 15:54:15 +00:00
} from "@/lib/api";
describe("webui API helpers", () => {
beforeEach(() => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ deleted: true, key: "websocket:chat-1", messages: [] }),
}),
);
});
it("percent-encodes websocket keys when fetching webui-thread snapshot", async () => {
await fetchWebuiThread("tok", "websocket:chat-1");
expect(fetch).toHaveBeenCalledWith(
"/api/sessions/websocket%3Achat-1/webui-thread",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
credentials: "same-origin",
}),
);
});
it("percent-encodes websocket keys when deleting a session", async () => {
await deleteSession("tok", "websocket:chat-1");
expect(fetch).toHaveBeenCalledWith(
"/api/sessions/websocket%3Achat-1/delete",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
it("serializes settings updates as a narrow query string", async () => {
await updateSettings("tok", {
modelPreset: "default",
model: "openrouter/test",
provider: "openrouter",
timezone: "Asia/Shanghai",
botName: "nanobot",
botIcon: "nb",
toolHintMaxLength: 120,
});
expect(fetch).toHaveBeenCalledWith(
"/api/settings/update?model_preset=default&model=openrouter%2Ftest&provider=openrouter&timezone=Asia%2FShanghai&bot_name=nanobot&bot_icon=nb&tool_hint_max_length=120",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
2026-05-06 14:15:36 +00:00
it("serializes provider settings updates without returning secrets", async () => {
await updateProviderSettings("tok", {
provider: "openrouter",
apiKey: "sk-or-test",
apiBase: "https://openrouter.ai/api/v1",
});
expect(fetch).toHaveBeenCalledWith(
"/api/settings/provider/update?provider=openrouter&api_key=sk-or-test&api_base=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
2026-05-09 06:48:26 +00:00
it("serializes web search settings updates", async () => {
await updateWebSearchSettings("tok", {
provider: "searxng",
baseUrl: "https://search.example.com",
maxResults: 8,
timeout: 45,
useJinaReader: false,
2026-05-09 06:48:26 +00:00
});
expect(fetch).toHaveBeenCalledWith(
"/api/settings/web-search/update?provider=searxng&base_url=https%3A%2F%2Fsearch.example.com&max_results=8&timeout=45&use_jina_reader=false",
2026-05-09 06:48:26 +00:00
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
it("serializes image generation settings updates", async () => {
await updateImageGenerationSettings("tok", {
enabled: true,
provider: "openrouter",
model: "openai/gpt-5.4-image-2",
defaultAspectRatio: "16:9",
defaultImageSize: "2K",
maxImagesPerTurn: 3,
});
expect(fetch).toHaveBeenCalledWith(
"/api/settings/image-generation/update?enabled=true&provider=openrouter&model=openai%2Fgpt-5.4-image-2&default_aspect_ratio=16%3A9&default_image_size=2K&max_images_per_turn=3",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
2026-05-22 22:25:12 +08:00
it("reads CLI Apps catalog and serializes actions", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
apps: [],
installed_count: 0,
catalog_updated_at: "2026-04-18",
}),
} as Response);
await expect(fetchCliApps("tok")).resolves.toMatchObject({ apps: [] });
expect(fetch).toHaveBeenCalledWith(
"/api/settings/cli-apps",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
await runCliAppAction("tok", "install", "gimp");
expect(fetch).toHaveBeenCalledWith(
"/api/settings/cli-apps/install?name=gimp",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
it("reads and writes persisted sidebar state", async () => {
const state = {
schema_version: 1,
pinned_keys: ["websocket:chat-1"],
archived_keys: ["websocket:old"],
title_overrides: { "websocket:chat-1": "Release" },
tags_by_key: {},
collapsed_groups: {},
view: {
density: "compact" as const,
show_previews: false,
show_timestamps: false,
show_archived: true,
sort: "updated_desc" as const,
},
updated_at: null,
};
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => state,
} as Response);
await expect(fetchSidebarState("tok")).resolves.toEqual(state);
expect(fetch).toHaveBeenCalledWith(
"/api/webui/sidebar-state",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
await updateSidebarState("tok", state);
const [url, init] = vi.mocked(fetch).mock.calls.at(-1)!;
expect(String(url).startsWith("/api/webui/sidebar-state/update?")).toBe(true);
expect(init).toEqual(expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}));
const encodedState = new URLSearchParams(String(url).split("?", 2)[1]).get("state");
expect(encodedState).toBeTruthy();
expect(JSON.parse(encodedState ?? "{}")).toMatchObject({
pinned_keys: ["websocket:chat-1"],
title_overrides: { "websocket:chat-1": "Release" },
});
});
2026-05-06 14:15:36 +00:00
it("maps generated session titles from the sessions list", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
sessions: [
{
key: "websocket:chat-1",
created_at: "2026-05-01T10:00:00",
updated_at: "2026-05-01T10:01:00",
title: "优化 WebUI 标题",
run_started_at: 1_700_000_000,
2026-05-06 14:15:36 +00:00
},
],
}),
} as Response);
await expect(listSessions("tok")).resolves.toMatchObject([
{
key: "websocket:chat-1",
title: "优化 WebUI 标题",
preview: "",
runStartedAt: 1_700_000_000,
2026-05-06 14:15:36 +00:00
},
]);
});
2026-05-06 15:54:15 +00:00
it("maps slash command metadata from the commands endpoint", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
commands: [
{
command: "/stop",
title: "Stop current task",
description: "Cancel the active task.",
icon: "square",
},
{
command: "/restart",
title: "Restart nanobot",
description: "Restart the bot process.",
icon: "rotate-cw",
},
2026-05-06 15:54:15 +00:00
{
command: "/history",
title: "Show conversation history",
description: "Print the last N messages.",
icon: "history",
arg_hint: "[n]",
},
],
}),
} as Response);
await expect(listSlashCommands("tok")).resolves.toEqual([
{
command: "/history",
title: "Show conversation history",
description: "Print the last N messages.",
icon: "history",
argHint: "[n]",
},
]);
expect(fetch).toHaveBeenCalledWith(
"/api/commands",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
});