256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
import { expect, it, vi } from "vitest";
|
|
import type { SettingsPayload } from "@/lib/types";
|
|
import { requestMutationMock, jsonResponse, settingsPayload, renderSettingsView, openPopover, installSettingsViewTestHooks } from "@/tests/settings-test-utils";
|
|
|
|
|
|
describe("Settings capabilities", () => {
|
|
installSettingsViewTestHooks();
|
|
|
|
|
|
it("selects image models from provider-specific options", async () => {
|
|
const base = settingsPayload();
|
|
const payload: SettingsPayload = {
|
|
...base,
|
|
image_generation: {
|
|
...base.image_generation,
|
|
providers: [
|
|
{
|
|
name: "openrouter",
|
|
label: "OpenRouter",
|
|
configured: true,
|
|
models: ["openai/gpt-5.4-image-2"],
|
|
default_model: "openai/gpt-5.4-image-2",
|
|
},
|
|
{
|
|
name: "gemini",
|
|
label: "Gemini",
|
|
configured: true,
|
|
models: ["gemini-2.5-flash-image", "imagen-4.0-generate-001"],
|
|
default_model: "gemini-2.5-flash-image",
|
|
},
|
|
{
|
|
name: "custom",
|
|
label: "Custom",
|
|
configured: true,
|
|
models: [],
|
|
default_model: null,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
renderSettingsView({ initialSection: "image", initialSettings: payload });
|
|
|
|
expect(screen.queryByDisplayValue("openai/gpt-5.4-image-2")).not.toBeInTheDocument();
|
|
fireEvent.pointerDown(screen.getByRole("button", { name: "OpenRouter" }));
|
|
fireEvent.click(await screen.findByRole("menuitem", { name: "Gemini" }));
|
|
|
|
expect(await screen.findByRole("button", { name: "gemini-2.5-flash-image" })).toBeInTheDocument();
|
|
await openPopover(screen.getByRole("button", { name: "gemini-2.5-flash-image" }));
|
|
fireEvent.click(await screen.findByRole("option", { name: "imagen-4.0-generate-001" }));
|
|
await waitFor(() =>
|
|
expect(screen.getByRole("button", { name: "imagen-4.0-generate-001" })).toBeInTheDocument(),
|
|
);
|
|
|
|
await openPopover(screen.getByRole("button", { name: "imagen-4.0-generate-001" }));
|
|
const modelInput = await screen.findByRole("combobox", { name: "Search or type model ID" });
|
|
fireEvent.change(modelInput, { target: { value: "imagen-5-preview" } });
|
|
fireEvent.click(await screen.findByRole("option", { name: "Use “imagen-5-preview”" }));
|
|
expect(await screen.findByRole("button", { name: "imagen-5-preview" })).toBeInTheDocument();
|
|
|
|
fireEvent.pointerDown(screen.getByRole("button", { name: "Gemini" }));
|
|
fireEvent.click(await screen.findByRole("menuitem", { name: "Custom" }));
|
|
expect(screen.getByRole("button", { name: "imagen-5-preview" })).toBeInTheDocument();
|
|
|
|
await openPopover(screen.getByRole("button", { name: "imagen-5-preview" }));
|
|
const customProviderInput = await screen.findByRole("combobox", {
|
|
name: "Search or type model ID",
|
|
});
|
|
fireEvent.change(customProviderInput, { target: { value: "private/image-v2" } });
|
|
fireEvent.keyDown(customProviderInput, { key: "Enter" });
|
|
expect(await screen.findByRole("button", { name: "private/image-v2" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("saves network safety without exposing technical SSRF copy", async () => {
|
|
const payload = settingsPayload();
|
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
if (url === "/api/settings") return jsonResponse(payload);
|
|
if (url === "/api/settings/cli-apps") {
|
|
return jsonResponse({ apps: [], installed_count: 0 });
|
|
}
|
|
if (url === "/api/settings/mcp-presets") {
|
|
return jsonResponse({ presets: [], installed_count: 0 });
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockResolvedValueOnce({
|
|
...payload,
|
|
advanced: { ...payload.advanced, webui_allow_local_service_access: false },
|
|
requires_restart: true,
|
|
restart_required_sections: ["runtime"],
|
|
});
|
|
|
|
renderSettingsView({ initialSection: "advanced" });
|
|
|
|
expect(await screen.findByText("Web safety")).toBeInTheDocument();
|
|
expect(screen.queryByText(/SSRF/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByText("Private Service Protection")).not.toBeInTheDocument();
|
|
expect(screen.getByText("Default access")).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "Restricted" })).not.toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Default Permission" })).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Full Access" })).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("switch", { name: "Local services" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
|
|
await waitFor(() =>
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.network_safety.update",
|
|
{
|
|
webui_allow_local_service_access: false,
|
|
webui_default_access_mode: "default",
|
|
},
|
|
20_000,
|
|
),
|
|
);
|
|
});
|
|
|
|
it("saves optional-key web search providers without an API key", async () => {
|
|
const payload = {
|
|
...settingsPayload(),
|
|
web_search: {
|
|
...settingsPayload().web_search,
|
|
provider: "duckduckgo",
|
|
providers: [
|
|
{ name: "duckduckgo", label: "DuckDuckGo", credential: "none" as const },
|
|
{ name: "keenable", label: "Keenable", credential: "optional_api_key" as const },
|
|
],
|
|
},
|
|
};
|
|
const updatedPayload = {
|
|
...payload,
|
|
web_search: {
|
|
...payload.web_search,
|
|
provider: "keenable",
|
|
},
|
|
};
|
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
if (url === "/api/settings") return jsonResponse(payload);
|
|
if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
|
|
if (url === "/api/settings/mcp-presets") return jsonResponse({ presets: [], installed_count: 0 });
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockResolvedValueOnce(updatedPayload);
|
|
|
|
renderSettingsView({ initialSection: "browser" });
|
|
|
|
fireEvent.pointerDown(await screen.findByRole("button", { name: /DuckDuckGo/ }));
|
|
fireEvent.click(await screen.findByRole("menuitem", { name: "Keenable" }));
|
|
const saveButton = screen
|
|
.getAllByRole("button", { name: "Save" })
|
|
.find((button) => !(button as HTMLButtonElement).disabled);
|
|
if (!saveButton) throw new Error("enabled Save button was not found");
|
|
fireEvent.click(saveButton);
|
|
|
|
await waitFor(() =>
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.web_search.update",
|
|
{
|
|
provider: "keenable",
|
|
max_results: 5,
|
|
timeout: 30,
|
|
use_jina_reader: true,
|
|
},
|
|
20_000,
|
|
),
|
|
);
|
|
});
|
|
|
|
it("uses native host safety copy on the native surface", async () => {
|
|
const payload = {
|
|
...settingsPayload(),
|
|
surface: "native" as const,
|
|
runtime_surface: "native" as const,
|
|
};
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
if (url === "/api/settings") return jsonResponse(payload);
|
|
if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
|
|
if (url === "/api/settings/mcp-presets") return jsonResponse({ presets: [], installed_count: 0 });
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
|
|
renderSettingsView({ initialSection: "advanced" });
|
|
|
|
expect(await screen.findByText("App safety")).toBeInTheDocument();
|
|
expect(screen.queryByText("Web safety")).not.toBeInTheDocument();
|
|
expect(screen.getByText("Allow Full Access shell commands to reach services on this Mac.")).toBeInTheDocument();
|
|
});
|
|
|
|
it("refreshes settings with a fresh token after native engine restart", async () => {
|
|
const payload = {
|
|
...settingsPayload(),
|
|
surface: "native" as const,
|
|
runtime_surface: "native" as const,
|
|
runtime_capabilities: {
|
|
can_restart_engine: true,
|
|
can_pick_folder: true,
|
|
can_open_logs: true,
|
|
can_export_diagnostics: true,
|
|
},
|
|
};
|
|
const restartedPayload = {
|
|
...payload,
|
|
advanced: { ...payload.advanced, webui_allow_local_service_access: false },
|
|
requires_restart: true,
|
|
restart_required_sections: ["runtime"],
|
|
};
|
|
const refreshedPayload = {
|
|
...restartedPayload,
|
|
requires_restart: false,
|
|
restart_required_sections: [],
|
|
};
|
|
const restartEngine = vi.fn(async () => "fresh-token");
|
|
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
const auth = (init?.headers as Record<string, string> | undefined)?.Authorization;
|
|
if (url === "/api/settings" && auth === "Bearer fresh-token") {
|
|
return jsonResponse(refreshedPayload);
|
|
}
|
|
if (url === "/api/settings") return jsonResponse(payload);
|
|
if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
|
|
if (url === "/api/settings/mcp-presets") return jsonResponse({ presets: [], installed_count: 0 });
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockResolvedValueOnce(restartedPayload);
|
|
|
|
renderSettingsView({
|
|
initialSection: "advanced",
|
|
onNativeEngineRestart: restartEngine,
|
|
});
|
|
|
|
expect(await screen.findByText("App safety")).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("switch", { name: "Local services" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
|
|
await waitFor(() => expect(restartEngine).toHaveBeenCalledTimes(1));
|
|
await waitFor(() =>
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"/api/settings",
|
|
expect.objectContaining({
|
|
headers: { Authorization: "Bearer fresh-token" },
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
});
|