Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/ui/typography.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ interface TextProps
*/
className?: string;

/** HTML id used to associate text with form controls and ARIA descriptions. */
id?: string;

/** Native browser tooltip text */
title?: string;
}
Expand Down
129 changes: 129 additions & 0 deletions src/routes/Settings/sections/AgentSettings.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { AgentSettings } from "./AgentSettings";

const STORAGE_KEY = "tangle.componentSearchV2.config";
const mockNotify = vi.fn();
const mockFetch = vi.fn();

vi.mock("@/hooks/useToastNotification", () => ({
default: () => mockNotify,
}));

describe("AgentSettings", () => {
beforeEach(() => {
window.localStorage.clear();
mockNotify.mockClear();
mockFetch.mockReset();
global.fetch = mockFetch;
});

afterEach(() => {
window.localStorage.clear();
});

it("shows inline feedback instead of saving when model is blank", () => {
render(<AgentSettings />);

fireEvent.change(screen.getByLabelText("API base URL"), {
target: { value: "https://api.example.com/v1" },
});
fireEvent.change(screen.getByLabelText("API key"), {
target: { value: "sk-test" },
});
fireEvent.change(screen.getByLabelText("Model id"), {
target: { value: " " },
});

fireEvent.click(screen.getByRole("button", { name: "Save" }));

expect(
screen.getByText("Enter a model id before saving."),
).toBeInTheDocument();
expect(screen.getByLabelText("Model id")).toHaveAttribute(
"aria-invalid",
"true",
);
expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
expect(mockNotify).not.toHaveBeenCalledWith(
"Agent settings saved",
"success",
);
});

it("validates that the configured model exists when testing connection", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({ data: [{ id: "gpt-4o-mini" }, { id: "o3-mini" }] }),
} as Response);
render(<AgentSettings />);

fireEvent.change(screen.getByLabelText("API base URL"), {
target: { value: "https://api.example.com/v1" },
});
fireEvent.change(screen.getByLabelText("API key"), {
target: { value: "sk-test" },
});
fireEvent.change(screen.getByLabelText("Model id"), {
target: { value: "gpt-4o-mini" },
});

fireEvent.click(screen.getByRole("button", { name: "Test connection" }));

await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
"Connected. Model “gpt-4o-mini” is available.",
"success",
);
});
});

it("reports an error when the configured model is missing from provider models", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [{ id: "gpt-4o-mini" }] }),
} as Response);
render(<AgentSettings />);

fireEvent.change(screen.getByLabelText("API base URL"), {
target: { value: "https://api.example.com/v1" },
});
fireEvent.change(screen.getByLabelText("API key"), {
target: { value: "sk-test" },
});
fireEvent.change(screen.getByLabelText("Model id"), {
target: { value: "missing-model" },
});

fireEvent.click(screen.getByRole("button", { name: "Test connection" }));

await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
"Connected, but model “missing-model” was not found.",
"error",
);
});
});

it("allows clearing partially configured settings", () => {
window.localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
apiBase: "https://api.example.com/v1",
apiKey: "sk-test",
}),
);

render(<AgentSettings />);

fireEvent.click(screen.getByRole("button", { name: "Clear" }));

expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
expect(mockNotify).toHaveBeenCalledWith(
"Agent settings cleared",
"success",
);
});
});
Loading
Loading