Files
OmniRoute/tests/unit/ui/edit-connection-modal-free-models.test.tsx
Ke Jin 769ab62fa3 fix(reasoning): preserve compatible response state (#10574)
Preserves authentic plaintext reasoning continuations across Chat Completions and Responses (streaming + non-streaming), applying one target-aware reasoning transport policy before protocol translation. Fixes #10550.

Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 39 files):
- 446/446 focused node:test tests pass (chat-route-coverage, chatcore-translation-paths, combo-attempt-body-isolation-7847, combo-config, executor-codex, kimi-coding-translator, moonshot-k3, reasoning-cache, response-sanitizer, responses-handler, responses-translation-fixes, strip-reasoning-blobs-agentic-context-1599, translator-openai-responses-req).
- 12/12 vitest tests pass (edit-connection-modal-free-models.test.tsx).
- check-changelog-integrity: OK.
- typecheck:core: clean.
- check-complexity / check-cognitive-complexity: OK, both under baseline.
- file-size: chatHelpers.ts crossed the frozen cap by +2 lines (irreducible reasoningTransportFallback option threading) — rebaselined 1017->1019 with justification, pushed to the PR branch (fix-in-place), re-validated after a base-drift re-merge against the latest release tip.

Co-authored-by: jackjinke <jackjinke@users.noreply.github.com>
2026-08-20 22:20:54 -03:00

339 lines
12 KiB
TypeScript

// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next/navigation", () => ({
useParams: () => ({ id: "openrouter" }),
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: EditConnectionModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal");
const FREE_TOGGLE = 'button[role="switch"][aria-label="importFreeModelsOnlyLabel"]';
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<EditConnectionModal
isOpen
providerId={(props.providerId as string) || "openrouter"}
onSave={async () => undefined}
onClose={() => {}}
{...(props as any)}
/>
);
});
containers.push({ root, el });
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
beforeEach(() => {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({ ok: true, json: async () => ({}), text: async () => "" } as Response)
)
);
vi.stubGlobal("localStorage", {
getItem: () => null,
setItem: () => undefined,
removeItem: () => undefined,
clear: () => undefined,
});
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("EditConnectionModal — import only free models", () => {
it("shows the free-only toggle for a connection whose provider has free models", () => {
const el = render({
connection: { id: "conn-1", provider: "openrouter", providerSpecificData: {} },
});
expect(el.querySelector(FREE_TOGGLE)).toBeTruthy();
});
it("hides the free-only toggle for a provider without free models", () => {
const el = render({
providerId: "anthropic",
connection: { id: "conn-2", provider: "anthropic", providerSpecificData: {} },
});
expect(el.querySelector(FREE_TOGGLE)).toBeNull();
});
it("reflects the persisted flag as initially checked", () => {
const el = render({
connection: {
id: "conn-3",
provider: "openrouter",
providerSpecificData: { importFreeModelsOnly: true },
},
});
expect(el.querySelector(FREE_TOGGLE)!.getAttribute("aria-checked")).toBe("true");
});
it("clears the flag (explicit false) and re-syncs when toggled off", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const onResyncModels = vi.fn().mockResolvedValue(undefined);
const el = render({
connection: {
id: "conn-5",
provider: "openrouter",
name: "OR",
providerSpecificData: { importFreeModelsOnly: true },
},
onSave,
onResyncModels,
});
const toggle = el.querySelector<HTMLButtonElement>(FREE_TOGGLE)!;
expect(toggle.getAttribute("aria-checked")).toBe("true");
act(() => {
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
// Must be an explicit `false`, not `undefined` — otherwise the PUT merge keeps the old `true`.
expect(onSave.mock.calls[0][0].providerSpecificData?.importFreeModelsOnly).toBe(false);
await waitFor(() => onResyncModels.mock.calls.length > 0);
expect(onResyncModels).toHaveBeenCalledWith("conn-5");
});
it("saves the flag and triggers a re-sync when toggled on", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const onResyncModels = vi.fn().mockResolvedValue(undefined);
const el = render({
connection: { id: "conn-4", provider: "openrouter", name: "OR", providerSpecificData: {} },
onSave,
onResyncModels,
});
const toggle = el.querySelector<HTMLButtonElement>(FREE_TOGGLE)!;
act(() => {
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.importFreeModelsOnly).toBe(true);
await waitFor(() => onResyncModels.mock.calls.length > 0);
expect(onResyncModels).toHaveBeenCalledWith("conn-4");
});
});
describe("EditConnectionModal — encrypted Responses reasoning", () => {
const PRESERVE_TOGGLE = 'button[role="switch"][aria-label="Preserve encrypted reasoning"]';
it("loads and saves the opt-in for an OpenAI-compatible Responses connection", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "openai-compatible-responses-12345678-1234-1234-1234-123456789abc",
connection: {
id: "conn-responses",
provider: "openai-compatible-responses-12345678-1234-1234-1234-123456789abc",
authType: "apikey",
providerSpecificData: { preserveEncryptedReasoning: true },
},
onSave,
});
const toggle = el.querySelector<HTMLButtonElement>(PRESERVE_TOGGLE)!;
expect(toggle.getAttribute("aria-checked")).toBe("true");
act(() => toggle.dispatchEvent(new MouseEvent("click", { bubbles: true })));
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "save"
)!;
act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitFor(() => onSave.mock.calls.length > 0);
expect(onSave.mock.calls[0][0].providerSpecificData?.preserveEncryptedReasoning).toBe(false);
});
it("hides the redundant opt-in for first-party OpenAI and removes stale state on save", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "openai",
connection: {
id: "conn-openai",
provider: "openai",
authType: "apikey",
providerSpecificData: { preserveEncryptedReasoning: true },
},
onSave,
});
expect(el.querySelector(PRESERVE_TOGGLE)).toBeNull();
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "save"
)!;
act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitFor(() => onSave.mock.calls.length > 0);
expect(onSave.mock.calls[0][0].providerSpecificData).not.toHaveProperty(
"preserveEncryptedReasoning"
);
});
it("is absent for a chat-only compatible connection", () => {
const el = render({
providerId: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
connection: {
id: "conn-chat",
provider: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
authType: "apikey",
providerSpecificData: {},
},
});
expect(el.querySelector(PRESERVE_TOGGLE)).toBeNull();
});
it("appears when a compatible connection selects the Responses target format", () => {
const el = render({
providerId: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
connection: {
id: "conn-selected-responses",
provider: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
authType: "apikey",
providerSpecificData: { targetFormat: "openai-responses" },
},
});
expect(el.querySelector(PRESERVE_TOGGLE)?.getAttribute("aria-checked")).toBe("false");
});
it("hides the redundant opt-in for Codex while keeping its effective controls", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "codex",
connection: {
id: "conn-codex",
provider: "codex",
authType: "oauth",
providerSpecificData: { preserveEncryptedReasoning: true },
},
onSave,
});
expect(el.querySelector(PRESERVE_TOGGLE)).toBeNull();
expect(el.textContent).toContain("defaultThinkingStrengthLabel");
expect(
el.querySelector('button[role="switch"][aria-label="openaiResponsesStoreLabel"]')
).toBeTruthy();
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "save"
)!;
act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitFor(() => onSave.mock.calls.length > 0);
expect(onSave.mock.calls[0][0].providerSpecificData).not.toHaveProperty(
"preserveEncryptedReasoning"
);
});
});
describe("EditConnectionModal — quota scraping fields", () => {
it("saves OpenCode Go workspace and replacement auth cookie", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "opencode-go",
connection: {
id: "conn-opencode-go",
provider: "opencode-go",
name: "OpenCode Go",
authType: "apikey",
providerSpecificData: { workspaceId: "workspace-existing" },
},
onSave,
});
const workspaceInput = el.querySelector<HTMLInputElement>(
'input[name="opencodeGoWorkspaceId"]'
)!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="opencodeGoAuthCookie"]')!;
expect(workspaceInput.value).toBe("workspace-existing");
setInputValue(workspaceInput, "workspace-updated");
setInputValue(cookieInput, "auth=opencode-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.opencodeGoWorkspaceId).toBe("workspace-updated");
expect(payload.providerSpecificData?.opencodeGoAuthCookie).toBe("auth=opencode-cookie");
});
it("omits Ollama Cloud usage cookie when the edit field is left blank", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "ollama-cloud",
connection: {
id: "conn-ollama-cloud",
provider: "ollama-cloud",
name: "Ollama Cloud",
authType: "apikey",
providerSpecificData: {},
},
onSave,
});
expect(el.querySelector<HTMLInputElement>('input[name="ollamaCloudUsageCookie"]')).toBeTruthy();
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect("ollamaCloudUsageCookie" in payload.providerSpecificData).toBe(false);
});
});