Skip to content
Merged
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
32 changes: 32 additions & 0 deletions __tests__/actions/models/create-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,36 @@ describe("createModel action", () => {
expect(res).toEqual({ ok: true, value: newModel });
expect(chainable.insert).toHaveBeenCalled();
});

it("inserts model with reasoningEffortFormat and modelOptions", async () => {
chainable.where.mockResolvedValueOnce([{ id: VALID_UUID }]);
const newModel = {
id: "model-2",
providerId: VALID_UUID,
userId: "user-1",
modelId: "claude-3-7-sonnet",
label: "Claude 3.7",
modelType: "chat",
contextWindow: 200000,
reasoningEffortFormat: "anthropic",
modelOptions: JSON.stringify({ temperature: 0.7 }),
isManuallyAdded: true,
isEnabled: true,
};
chainable.returning.mockResolvedValueOnce([newModel]);

const res = await createModel({
providerId: VALID_UUID,
modelId: "claude-3-7-sonnet",
label: "Claude 3.7",
modelType: "chat",
contextWindow: 200000,
capReasoning: true,
reasoningEffortFormat: "anthropic",
modelOptions: { temperature: 0.7 },
});

expect(res).toEqual({ ok: true, value: newModel });
expect(chainable.insert).toHaveBeenCalled();
});
});
60 changes: 60 additions & 0 deletions __tests__/actions/models/sync-provider-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,4 +636,64 @@ describe("syncProviderModels action", () => {
error: expect.objectContaining({ code: "INTERNAL" }),
});
});

it("detects reasoning effort format for openrouter, openai, and pattern-matched models", async () => {
chainable.where
.mockResolvedValueOnce([
{ id: "prov-1", baseUrl: "https://openrouter.ai/api/v1" },
])
.mockResolvedValueOnce([]); // No existing models

const fetchMock = vi
.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: [
{
id: "openrouter/model-a",
supported_parameters: ["include_reasoning"],
},
{
id: "openai/model-b",
supported_parameters: ["reasoning_effort"],
},
{
id: "deepseek/deepseek-r1",
supported_parameters: [],
},
],
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: [] }),
});

vi.stubGlobal("fetch", fetchMock);

const result = await syncProviderModels("prov-1");
expect(result.ok).toBe(true);
expect(chainable.values).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
modelId: "openrouter/model-a",
capReasoning: true,
reasoningEffortFormat: "openrouter",
}),
expect.objectContaining({
modelId: "openai/model-b",
capReasoning: true,
reasoningEffortFormat: "openai",
}),
expect.objectContaining({
modelId: "deepseek/deepseek-r1",
capReasoning: true,
reasoningEffortFormat: "none",
}),
]),
);

vi.unstubAllGlobals();
});
});
5 changes: 4 additions & 1 deletion __tests__/actions/models/update-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,13 @@ describe("updateModels action", () => {
await updateModels("model-1", { capReasoning: false });
await updateModels("model-1", { capStructuredOutput: true });
await updateModels("model-1", { capStructuredOutput: false });
await updateModels("model-1", { reasoningEffortFormat: "openai" });
await updateModels("model-1", { modelOptions: { temperature: 0.7 } });
await updateModels("model-1", { modelOptions: null });
await updateModels("model-1", { isEnabled: true });
await updateModels("model-1", { isEnabled: false });
await updateModels(["model-1"], { label: "Single Array" });

expect(chainable.set).toHaveBeenCalledTimes(16);
expect(chainable.set).toHaveBeenCalledTimes(19);
});
});
41 changes: 41 additions & 0 deletions __tests__/actions/providers/export-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,45 @@ describe("exportProviderRegistry", () => {
expect(result.providers[0].models[2].modelType).toBe("chat");
expect(result.providers[0].models[3].modelType).toBe("chat");
});

it("handles modelOptions with valid JSON and catches malformed JSON", async () => {
const modelsWithOpts = [
{
...MODEL_ROW,
id: "m-opts-1",
reasoningEffortFormat: "anthropic",
modelOptions: JSON.stringify({ temperature: 0.7 }),
},
{
...MODEL_ROW,
id: "m-opts-2",
reasoningEffortFormat: null,
modelOptions: "invalid-json{",
},
{
...MODEL_ROW,
id: "m-opts-3",
reasoningEffortFormat: "openai",
modelOptions: null,
},
];

chainable.where
.mockResolvedValueOnce([PROVIDER_ROW])
.mockResolvedValueOnce(modelsWithOpts);

const res = await exportProviderRegistry();
expect(res.ok).toBe(true);
if (!res.ok) return;

const exportedModels = res.value.providers[0].models;
expect(exportedModels[0].reasoningEffortFormat).toBe("anthropic");
expect(exportedModels[0].modelOptions).toEqual({ temperature: 0.7 });

expect(exportedModels[1].reasoningEffortFormat).toBe("none");
expect(exportedModels[1].modelOptions).toBeNull();

expect(exportedModels[2].reasoningEffortFormat).toBe("openai");
expect(exportedModels[2].modelOptions).toBeNull();
});
});
2 changes: 2 additions & 0 deletions __tests__/actions/providers/import-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ describe("importProviderRegistry action", () => {
structuredOutput: false,
},
isManuallyAdded: true,
reasoningEffortFormat: "openai",
modelOptions: { dimensions: 1536 },
},
],
},
Expand Down
26 changes: 26 additions & 0 deletions __tests__/app/api/chat/route-rate-limit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,32 @@ describe("POST /api/chat — rate limiting (T9.1)", () => {
});
});

it("forwards reasoningEffort when provided in chat request payload", async () => {
vi.mocked(checkRateLimit).mockReturnValue({
allowed: true,
retryAfterSeconds: 0,
});
const req = new Request("http://localhost/api/chat", {
method: "POST",
body: JSON.stringify({
chatId: "11111111-1111-4111-8111-111111111111",
userMessageId: "22222222-2222-4222-8222-222222222222",
model: "o3-mini",
reasoningEffort: "high",
}),
});

const res = await POST(req);
expect(res.status).toBe(200);
expect(inngest.send).toHaveBeenCalledWith({
name: "chat/response.generate",
data: expect.objectContaining({
model: "o3-mini",
reasoningEffort: "high",
}),
});
});

it("returns 500 when database or inngest dispatch throws an error", async () => {
vi.mocked(checkRateLimit).mockReturnValue({
allowed: true,
Expand Down
121 changes: 121 additions & 0 deletions __tests__/app/settings/models/model-detail-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";

const {
mockPush,
mockNotFound,
mockUseProviders,
mockUseUserModels,
mockDeleteModel,
} = vi.hoisted(() => ({
mockPush: vi.fn(),
mockNotFound: vi.fn(),
mockUseProviders: vi.fn(),
mockUseUserModels: vi.fn(),
mockDeleteModel: vi.fn(),
}));

vi.mock("next/navigation", () => ({
useParams: () => ({ id: "m1" }),
useRouter: () => ({ push: mockPush, refresh: vi.fn() }),
notFound: () => mockNotFound(),
}));

vi.mock("nuqs", () => ({
parseAsString: {
withDefault: vi.fn().mockReturnValue({
withOptions: vi.fn().mockReturnValue({}),
}),
},
useQueryState: vi.fn((_key, _options) => ["details", vi.fn()]),
}));

vi.mock("@/hooks/use-providers", () => ({
useProviders: () => mockUseProviders(),
}));

vi.mock("@/hooks/use-user-models", () => ({
useUserModels: () => mockUseUserModels(),
}));

vi.mock("@/actions/models/delete-model", () => ({
deleteModel: (id: string) => mockDeleteModel(id),
}));

vi.mock("@/actions/models/update-model", () => ({
updateModels: vi.fn().mockResolvedValue({ ok: true, value: {} }),
}));

vi.mock("@/lib/providers/invalidate-provider-registry-cache", () => ({
invalidateProviderRegistryCache: vi.fn(),
}));

import ModelDetailPage from "@/app/settings/models/[id]/page";

describe("ModelDetailPage", () => {
const dummyModel = {
id: "m1",
userId: "u1",
providerId: "p1",
modelId: "openai/gpt-4o",
label: "GPT-4o",
modelType: "chat" as const,
contextWindow: 128000,
embeddingDimensions: null,
capTools: true,
capVision: true,
capReasoning: false,
capStructuredOutput: true,
reasoningEffortFormat: "none" as const,
modelOptions: null,
isEnabled: true,
providerName: "OpenRouter",
createdAt: new Date(),
updatedAt: new Date(),
};

beforeEach(() => {
vi.clearAllMocks();
mockUseProviders.mockReturnValue({
providers: [{ id: "p1", name: "OpenRouter" }],
isLoading: false,
});
mockUseUserModels.mockReturnValue({
rawModels: [dummyModel],
isLoading: false,
refresh: vi.fn(),
});
mockDeleteModel.mockResolvedValue({ ok: true, value: {} });
});

it("renders model details page and breadcrumb", () => {
render(<ModelDetailPage />);

expect(screen.getByText("GPT-4o")).toBeInTheDocument();
expect(screen.getByText("Back to Models")).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /details/i })).toBeInTheDocument();
});

it("calls notFound when model is not found in rawModels", () => {
mockUseUserModels.mockReturnValue({
rawModels: [],
isLoading: false,
refresh: vi.fn(),
});

render(<ModelDetailPage />);
expect(mockNotFound).toHaveBeenCalled();
});

it("shows loading skeleton when data is fetching", () => {
mockUseUserModels.mockReturnValue({
rawModels: [],
isLoading: true,
refresh: vi.fn(),
});

render(<ModelDetailPage />);
expect(screen.queryByText("Back to Models")).not.toBeInTheDocument();
});
});
69 changes: 69 additions & 0 deletions __tests__/app/settings/models/new-model-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";

const { mockPush, mockUseProviders } = vi.hoisted(() => ({
mockPush: vi.fn(),
mockUseProviders: vi.fn(),
}));

vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush, refresh: vi.fn() }),
}));

vi.mock("nuqs", () => ({
parseAsString: {
withDefault: vi.fn().mockReturnValue({
withOptions: vi.fn().mockReturnValue({}),
}),
},
useQueryState: vi.fn((_key, _options) => ["details", vi.fn()]),
}));

vi.mock("@/hooks/use-providers", () => ({
useProviders: () => mockUseProviders(),
}));

vi.mock("@/actions/models/create-model", () => ({
createModel: vi.fn().mockResolvedValue({ ok: true, value: { id: "m-new" } }),
}));

vi.mock("@/lib/providers/invalidate-provider-registry-cache", () => ({
invalidateProviderRegistryCache: vi.fn(),
}));

import NewModelPage from "@/app/settings/models/new/page";

describe("NewModelPage", () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseProviders.mockReturnValue({
providers: [
{
id: "p1",
name: "OpenRouter",
baseUrl: "https://openrouter.ai/api/v1",
},
],
isLoading: false,
});
});

it("renders page header and model editor form", () => {
render(<NewModelPage />);

expect(screen.getByText("Add Model")).toBeInTheDocument();
expect(screen.getByText("Back to Models")).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /details/i })).toBeInTheDocument();
});

it("shows loading skeleton when providers are loading", () => {
mockUseProviders.mockReturnValue({
providers: [],
isLoading: true,
});

render(<NewModelPage />);
expect(screen.queryByText("Add Model")).not.toBeInTheDocument();
});
});
Loading
Loading