mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 14:22:14 +03:00
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>
339 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|