feat(ui): improve Lighthouse v2 configuration

This commit is contained in:
alejandrobailo
2026-06-25 10:49:36 +02:00
parent c80dd98fde
commit 14edac25a1
2 changed files with 1368 additions and 263 deletions
@@ -0,0 +1,356 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type {
LighthouseV2Configuration,
LighthouseV2SupportedModel,
LighthouseV2SupportedProvider,
} from "@/types/lighthouse-v2";
import { LighthouseV2ConfigPage } from "./lighthouse-v2-config-page";
const {
createConfigurationMock,
deleteConfigurationMock,
refreshMock,
testConnectionMock,
updateConfigurationMock,
} = vi.hoisted(() => ({
createConfigurationMock: vi.fn(),
deleteConfigurationMock: vi.fn(),
refreshMock: vi.fn(),
testConnectionMock: vi.fn(),
updateConfigurationMock: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({
refresh: refreshMock,
}),
}));
vi.mock("@/actions/lighthouse-v2/lighthouse-v2", () => ({
createLighthouseV2Configuration: createConfigurationMock,
deleteLighthouseV2Configuration: deleteConfigurationMock,
testLighthouseV2ConfigurationConnection: testConnectionMock,
updateLighthouseV2Configuration: updateConfigurationMock,
}));
const providers: LighthouseV2SupportedProvider[] = [
{ id: "openai", name: "OpenAI" },
{ id: "bedrock", name: "Amazon Bedrock" },
{ id: "openai-compatible", name: "OpenAI-compatible" },
];
const modelsByProvider = {
openai: [
model("gpt-5.1", {
supportsFunctionCalling: true,
supportsVision: true,
supportsReasoning: true,
}),
],
bedrock: [
model("anthropic.claude-4", {
supportsFunctionCalling: true,
supportsVision: false,
supportsReasoning: true,
}),
],
"openai-compatible": [model("llama-3.3")],
};
const configurations: LighthouseV2Configuration[] = [
{
id: "config-openai",
providerType: "openai",
baseUrl: null,
defaultModel: "gpt-5.1",
businessContext: "Production account",
connected: true,
connectionLastCheckedAt: "2026-06-24T10:00:00Z",
insertedAt: "2026-06-24T09:00:00Z",
updatedAt: "2026-06-24T10:00:00Z",
},
{
id: "config-bedrock",
providerType: "bedrock",
baseUrl: null,
defaultModel: "anthropic.claude-4",
businessContext: "AWS landing zone",
connected: false,
connectionLastCheckedAt: "2026-06-23T10:00:00Z",
insertedAt: "2026-06-23T09:00:00Z",
updatedAt: "2026-06-23T10:00:00Z",
},
];
describe("LighthouseV2ConfigPage", () => {
beforeEach(() => {
createConfigurationMock.mockReset();
deleteConfigurationMock.mockReset();
refreshMock.mockReset();
testConnectionMock.mockReset();
updateConfigurationMock.mockReset();
createConfigurationMock.mockResolvedValue({ data: configurations[0] });
deleteConfigurationMock.mockResolvedValue({ data: true });
testConnectionMock.mockResolvedValue({
data: {
id: "task-1",
name: "lighthouse-config-connection",
state: "PENDING",
insertedAt: "2026-06-24T10:01:00Z",
completedAt: null,
},
});
updateConfigurationMock.mockResolvedValue({ data: configurations[0] });
});
it("renders provider readiness, statuses, and the active provider", () => {
// Given / When
renderPage();
// Then
expect(
screen.getByRole("heading", { name: "Lighthouse readiness" }),
).toBeInTheDocument();
expect(screen.getByText("1 connected")).toBeInTheDocument();
expect(screen.getByText("1 failed")).toBeInTheDocument();
expect(screen.getByText("1 not tested")).toBeInTheDocument();
const openAIProvider = screen.getByRole("button", { name: "OpenAI" });
expect(openAIProvider).toHaveAttribute("aria-pressed", "true");
expect(within(openAIProvider).getByText("Connected")).toBeInTheDocument();
const bedrockProvider = screen.getByRole("button", {
name: /Amazon Bedrock/i,
});
expect(within(bedrockProvider).getByText("Failed")).toBeInTheDocument();
const compatibleProvider = screen.getByRole("button", {
name: /OpenAI-compatible/i,
});
expect(
within(compatibleProvider).getByText("Not tested"),
).toBeInTheDocument();
});
it("loads provider-specific form values when switching provider", async () => {
// Given
const user = userEvent.setup();
renderPage();
// When
await user.click(screen.getByRole("button", { name: /Amazon Bedrock/i }));
// Then
expect(
screen.getByRole("textbox", { name: /Business context/i }),
).toHaveValue("AWS landing zone");
expect(
screen.getByRole("combobox", { name: "Default model" }),
).toHaveTextContent("anthropic.claude-4");
});
it("updates an existing configuration without sending blank credentials", async () => {
// Given
const user = userEvent.setup();
updateConfigurationMock.mockResolvedValue({
data: {
...configurations[0],
businessContext: "Updated context",
},
});
renderPage();
// When
await user.clear(
screen.getByRole("textbox", { name: /Business context/i }),
);
await user.type(
screen.getByRole("textbox", { name: /Business context/i }),
"Updated context",
);
await user.click(screen.getByRole("button", { name: /^Save$/i }));
// Then
await waitFor(() =>
expect(updateConfigurationMock).toHaveBeenCalledWith(
"config-openai",
expect.not.objectContaining({ credentials: expect.anything() }),
),
);
});
it("creates a new OpenAI-compatible configuration with required credentials", async () => {
// Given
const user = userEvent.setup();
const createdConfig: LighthouseV2Configuration = {
id: "config-compatible",
providerType: "openai-compatible",
baseUrl: "https://llm.example.com/v1",
defaultModel: "llama-3.3",
businessContext: "Private model",
connected: null,
connectionLastCheckedAt: null,
insertedAt: "2026-06-24T10:00:00Z",
updatedAt: "2026-06-24T10:00:00Z",
};
createConfigurationMock.mockResolvedValue({ data: createdConfig });
renderPage();
// When
await user.click(
screen.getByRole("button", { name: /OpenAI-compatible/i }),
);
await user.type(screen.getByLabelText("API key"), "provider-key");
await user.type(
screen.getByLabelText("Base URL"),
"https://llm.example.com/v1",
);
await user.type(
screen.getByRole("textbox", { name: /Business context/i }),
"Private model",
);
await user.click(screen.getByRole("button", { name: /^Save$/i }));
// Then
await waitFor(() =>
expect(createConfigurationMock).toHaveBeenCalledWith({
providerType: "openai-compatible",
credentials: { api_key: "provider-key" },
baseUrl: "https://llm.example.com/v1",
defaultModel: null,
businessContext: "Private model",
}),
);
});
it("blocks OpenAI-compatible save when base URL is missing", async () => {
// Given
const user = userEvent.setup();
renderPage();
// When
await user.click(
screen.getByRole("button", { name: /OpenAI-compatible/i }),
);
await user.type(screen.getByLabelText("API key"), "provider-key");
await user.click(screen.getByRole("button", { name: /^Save$/i }));
// Then
expect(
await screen.findByText(
"Base URL is required for OpenAI-compatible providers.",
),
).toBeInTheDocument();
expect(createConfigurationMock).not.toHaveBeenCalled();
});
it("shows Bedrock access key, secret key, and region fields", async () => {
// Given
const user = userEvent.setup();
renderPage();
// When
await user.click(screen.getByRole("button", { name: /Amazon Bedrock/i }));
// Then
expect(screen.getByLabelText("AWS access key ID")).toBeInTheDocument();
expect(screen.getByLabelText("AWS secret access key")).toBeInTheDocument();
expect(screen.getByLabelText("AWS region")).toBeInTheDocument();
});
it("shows the business context counter and blocks values over 1000 characters", async () => {
// Given
const user = userEvent.setup();
renderPage();
// When
const businessContext = screen.getByRole("textbox", {
name: /Business context/i,
});
await user.clear(businessContext);
await user.type(businessContext, "a".repeat(1001));
await user.click(screen.getByRole("button", { name: /^Save$/i }));
// Then
expect(screen.getByText("1001/1000")).toBeInTheDocument();
expect(
await screen.findByText(
"Business context cannot exceed 1000 characters.",
),
).toBeInTheDocument();
expect(updateConfigurationMock).not.toHaveBeenCalled();
});
it("starts a connection test and exposes a refresh status action", async () => {
// Given
const user = userEvent.setup();
renderPage();
// When
await user.click(screen.getByRole("button", { name: /Test connection/i }));
// Then
await waitFor(() =>
expect(testConnectionMock).toHaveBeenCalledWith("config-openai"),
);
expect(
await screen.findByText("Connection check started."),
).toBeInTheDocument();
await user.click(
screen.getAllByRole("button", { name: /Refresh status/i })[0],
);
expect(refreshMock).toHaveBeenCalledTimes(1);
});
it("confirms before deleting an existing configuration", async () => {
// Given
const user = userEvent.setup();
renderPage();
// When
await user.click(screen.getByRole("button", { name: /^Delete$/i }));
await user.click(
screen.getByRole("button", { name: /Delete configuration/i }),
);
// Then
await waitFor(() =>
expect(deleteConfigurationMock).toHaveBeenCalledWith("config-openai"),
);
expect(screen.getByText("Configuration removed.")).toBeInTheDocument();
});
});
function renderPage(
props?: Partial<Parameters<typeof LighthouseV2ConfigPage>[0]>,
) {
return render(
<LighthouseV2ConfigPage
configurations={props?.configurations ?? configurations}
providers={props?.providers ?? providers}
modelsByProvider={props?.modelsByProvider ?? modelsByProvider}
error={props?.error}
/>,
);
}
function model(
id: string,
overrides: Partial<LighthouseV2SupportedModel> = {},
): LighthouseV2SupportedModel {
return {
id,
maxInputTokens: null,
maxOutputTokens: null,
supportsFunctionCalling: null,
supportsVision: null,
supportsReasoning: null,
...overrides,
};
}
File diff suppressed because it is too large Load Diff