mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 13:14:56 +03:00
* fix(quality): green release/v3.8.50 base-reds round 2 — gateways/conol/deepai corruption, migrations, docs, ratchets, dashboard-typecheck Base-red fix for issue #9985 after the 2026-08-11 merge storm (99 PRs). Real defects fixed: - gateways.ts: close regolo entry (was swallowing naga-ac + chatanywhere from #9421), drop stale duplicate chatanywhere entry (#9594) - conol-web + deepai registry: correct ../shared import depth + deepai executor:default - modelSelectModalHelpers: close isProviderModelHidden (#9011) - driverFactory.test.ts: restore eaten test-closing brace (#9173) - usageTracking: remove duplicate cache_* props - modelCapability{Overrides,ResolutionSnapshot,Capabilities}: max_token -> max_output_tokens (#9199 vs #8908) + test align - videoGeneration: drop duplicate handleFalVideoGeneration import (mediaGeneration/fal canonical, #9982) - responseSanitizer: cast input_tokens_details before .cached_tokens access - EditConnectionModal: missing alibaba code fields, hoist validationPsd, providerPageHelpers Badge variant union - FreeBudgetCard: t() -> labels.noApiKey - peerRouting + cliRuntime: ProcessEnv typing - image-combo.test.ts: type any -> unknown - fal.test.ts: moved to tests/unit/services (collected path) 14 tests green - remove duplicate 143_job_registry.sql (146 canonical), KNOWN_GAPS fix Docs/ratchets (owner-authorized rebaselines, annotated): - CHANGELOG 3.8.50 living section restored + 42 i18n mirrors - MCP-SERVER.md 104->105 tools + i18n - ENVIRONMENT.md/.env.example: ADOBE_FIREFLY_CHROME_HEADED + DEBUG_CLAUDE_NONSTREAM - fabricated-docs allowlist: TELEGRAM proposal env vars - file-size: 5 grown files + proxyFetch 1207->1220 - dead-code 230->248, codeql 2->9 (drift from merged PRs, not this PR) - untrack _tasks symlink; agent-skills-sync --apply (config-codex-cli) * fix(changelog): reformat two feature fragments to the bullet convention (#9239, #9490) * fix(quality): prune stale ESLint suppressions (base-red) * fix(quality): resolve open-sse type errors + catalog/build regressions (base-red round 3) Storm-merge splices repaired in the base-fix PR #10131: - doctor.ts: AppConfig missing brokerSocketPath - conol-web.ts: Buffer not assignable to BodyInit (Uint8Array) - tinycms.ts: TinyCmsExecutor.execute return matches BaseExecutor (response/url/transformedBody) - tinycmsSigner.ts: encodeInto never-narrowing guard + dead wasm URL fallback (Turbopack) - virtualFactory.ts: options slot for resolutionSnapshot - bottleneckPatch.ts: insufficient-overlap casts (as unknown as) - imageCombo.ts: narrow handleImageGeneration union result - browser-worker.ts: AppConfig + turn.capabilities splice - conolDiscovery.ts: getProviderOutboundGuard from Policy module - catalog.ts: drop removed SWR hooks (getCatalogStaleWhileRevalidateMs + accessors), CatalogCachePolicy -> inline settings, resolve 4-arg call - catalogCache.ts: remove dead inFlight/promise refs - chat.ts: add isProviderBreakerFailureStatus import - model-catalog-cache-swr-8728.test.ts: align to #9199 new API (policy injection removed) * fix(quality): align UI test fixtures to current component contracts (base-red vitest) - setup-wizard: provide required serverState prop (component gained it in a merged PR) - grok-device-oauth-modal: next-intl stub resolves grok flow keys to EN labels - provider-quota-widget: label now inline (PR #8916 removed AutoRefreshButtonLabel extraction) — test the widget - use-provider-connections-cursor-refresh + phase1f: match /api/providers?provider=<id> query form; hoist heavy dynamic imports to module scope (timeout flake) - home-topology: mock next/navigation useRouter (component added node-click navigation) - cooling/lobe/AutoComboCatalog: raise cold-import describe timeouts to 30-60s - request-logger-*: align to current detail-view contract * fix(search): guard params.token undefined in serper headers (typecheck base-red) * fix(search): guard token headers + non-null providerConfig (typecheck base-red) * fix(changelog): restore base CHANGELOGs eaten by merge auto-resolve (43 files) --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: backryun <bakryun0718@proton.me>
394 lines
12 KiB
TypeScript
394 lines
12 KiB
TypeScript
// @vitest-environment jsdom
|
|
import React, { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
type ReplaceOptions = { scroll?: boolean };
|
|
type Replace = (url: string, options?: ReplaceOptions) => void;
|
|
|
|
const routerControl = vi.hoisted(() => ({
|
|
pendingUrl: null as string | null,
|
|
bumpPageRender: () => {},
|
|
replace: vi.fn<Replace>(),
|
|
}));
|
|
|
|
vi.mock("next-intl", () => ({
|
|
useLocale: () => "en",
|
|
useTranslations: (namespace?: string) => (key: string) => {
|
|
if (namespace === "requestLogger.detail") {
|
|
return { ariaLabel: "Request log detail", close: "Close detail modal" }[key] ?? key;
|
|
}
|
|
return key;
|
|
},
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({
|
|
replace: routerControl.replace,
|
|
push: vi.fn(),
|
|
prefetch: vi.fn(),
|
|
refresh: vi.fn(),
|
|
}),
|
|
usePathname: () => "/dashboard/logs",
|
|
useSearchParams: () => new URLSearchParams(globalThis.location.search),
|
|
}));
|
|
|
|
vi.mock("@/store/emailPrivacyStore", () => ({
|
|
default: () => ({ emailsVisible: true }),
|
|
}));
|
|
|
|
vi.mock("@/shared/components", async () => {
|
|
const { default: RequestLoggerV2 } =
|
|
await import("../../../src/shared/components/RequestLoggerV2.tsx");
|
|
const ConfirmModal = ({ isOpen }: { isOpen: boolean }) =>
|
|
isOpen ? <div data-testid="confirm-modal" /> : null;
|
|
return { RequestLoggerV2, ConfirmModal };
|
|
});
|
|
|
|
const { default: LogsPage } = await import("../../../src/app/(dashboard)/dashboard/logs/page.tsx");
|
|
|
|
function Harness() {
|
|
const [, setVersion] = React.useState(0);
|
|
|
|
React.useEffect(() => {
|
|
routerControl.bumpPageRender = () => setVersion((version) => version + 1);
|
|
return () => {
|
|
routerControl.bumpPageRender = () => {};
|
|
};
|
|
}, []);
|
|
|
|
return <LogsPage />;
|
|
}
|
|
|
|
function commitPendingUrl() {
|
|
if (routerControl.pendingUrl !== null) {
|
|
window.history.replaceState(null, "", routerControl.pendingUrl);
|
|
routerControl.pendingUrl = null;
|
|
}
|
|
}
|
|
|
|
class FakeIntersectionObserver {
|
|
static instances: FakeIntersectionObserver[] = [];
|
|
|
|
private active = true;
|
|
|
|
constructor(private readonly callback: IntersectionObserverCallback) {
|
|
FakeIntersectionObserver.instances.push(this);
|
|
}
|
|
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {
|
|
this.active = false;
|
|
}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
|
|
static triggerLatest() {
|
|
const instance = [...FakeIntersectionObserver.instances].reverse().find((item) => item.active);
|
|
if (!instance) throw new Error("No active IntersectionObserver");
|
|
instance.callback([{ isIntersecting: true } as IntersectionObserverEntry], instance as never);
|
|
}
|
|
}
|
|
|
|
const LOG_ROWS = Array.from({ length: 120 }, (_, index) => ({
|
|
id: `log-${String(index).padStart(3, "0")}`,
|
|
status: 200,
|
|
method: "POST",
|
|
path: "/v1/chat/completions",
|
|
timestamp: new Date(Date.UTC(2026, 0, 1, 12, 0) - index * 60_000).toISOString(),
|
|
model: `model-${String(index).padStart(3, "0")}`,
|
|
requestedModel: `model-${String(index).padStart(3, "0")}`,
|
|
provider: "openai",
|
|
account: "user@example.com",
|
|
tokens: { in: index + 1, out: index + 2 },
|
|
duration: 1_000 + index,
|
|
}));
|
|
|
|
let container: HTMLElement;
|
|
let root: Root;
|
|
let deferredDetail: {
|
|
id: string;
|
|
promise: Promise<Response>;
|
|
resolve: (response: Response) => void;
|
|
} | null;
|
|
let callLogUrls: string[];
|
|
|
|
function createDeferredDetail(id: string) {
|
|
let resolve!: (response: Response) => void;
|
|
const promise = new Promise<Response>((done) => {
|
|
resolve = done;
|
|
});
|
|
return { id, promise, resolve };
|
|
}
|
|
|
|
async function settle() {
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
}
|
|
|
|
function setInputValue(input: HTMLInputElement, value: string) {
|
|
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
setter?.call(input, value);
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
}
|
|
|
|
function setSelectValue(select: HTMLSelectElement, value: string) {
|
|
const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set;
|
|
setter?.call(select, value);
|
|
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}
|
|
|
|
function findButton(text: string) {
|
|
return Array.from(container.querySelectorAll<HTMLButtonElement>("button")).find((button) =>
|
|
button.textContent?.includes(text)
|
|
);
|
|
}
|
|
|
|
function getScrollContainer() {
|
|
const table = container.querySelector("table");
|
|
const scrollContainer = table?.parentElement as HTMLDivElement | null;
|
|
expect(scrollContainer).not.toBeNull();
|
|
return scrollContainer!;
|
|
}
|
|
|
|
function assertRetainedView(scrollContainer: HTMLDivElement) {
|
|
const search = container.querySelector<HTMLInputElement>(
|
|
'input[placeholder="searchPlaceholder"]'
|
|
);
|
|
const sort = container.querySelector<HTMLSelectElement>('select[title="sortLogs"]');
|
|
const successFilter = findButton("statusFilters.success");
|
|
const rows = container.querySelectorAll("tbody tr");
|
|
|
|
expect(search?.value).toBe("model");
|
|
expect(sort?.value).toBe("oldest");
|
|
expect(successFilter?.className).toContain("bg-emerald-500/20");
|
|
expect(rows).toHaveLength(100);
|
|
expect(rows[0]?.textContent).toContain("model-099");
|
|
expect(scrollContainer.scrollTop).toBe(337);
|
|
expect(
|
|
callLogUrls.some((url) => new URL(url, "http://test").searchParams.get("limit") === "100")
|
|
).toBe(true);
|
|
}
|
|
|
|
async function renderExpandedView() {
|
|
window.history.replaceState(null, "", "/dashboard/logs?view=requests&tenant=kept");
|
|
|
|
await act(async () => {
|
|
root.render(<Harness />);
|
|
});
|
|
await settle();
|
|
|
|
const search = container.querySelector<HTMLInputElement>(
|
|
'input[placeholder="searchPlaceholder"]'
|
|
);
|
|
const successFilter = findButton("statusFilters.success");
|
|
const sort = container.querySelector<HTMLSelectElement>('select[title="sortLogs"]');
|
|
expect(search).not.toBeNull();
|
|
expect(successFilter).not.toBeUndefined();
|
|
expect(sort).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
setInputValue(search!, "model");
|
|
successFilter!.click();
|
|
setSelectValue(sort!, "oldest");
|
|
});
|
|
await settle();
|
|
|
|
const scrollContainer = getScrollContainer();
|
|
await act(async () => {
|
|
scrollContainer.scrollTop = 120;
|
|
scrollContainer.dispatchEvent(new Event("scroll"));
|
|
FakeIntersectionObserver.triggerLatest();
|
|
});
|
|
await settle();
|
|
|
|
await act(async () => {
|
|
scrollContainer.scrollTop = 337;
|
|
scrollContainer.dispatchEvent(new Event("scroll"));
|
|
});
|
|
assertRetainedView(scrollContainer);
|
|
return scrollContainer;
|
|
}
|
|
|
|
async function openOlderRow() {
|
|
const row = Array.from(container.querySelectorAll<HTMLTableRowElement>("tbody tr")).find((item) =>
|
|
item.textContent?.includes("model-080")
|
|
);
|
|
expect(row).not.toBeUndefined();
|
|
|
|
await act(async () => {
|
|
row!.click();
|
|
});
|
|
await settle();
|
|
expect(container.querySelector('[aria-label="Request log detail"]')).not.toBeNull();
|
|
}
|
|
|
|
beforeEach(() => {
|
|
const storage = new Map<string, string>();
|
|
vi.stubGlobal("localStorage", {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => storage.set(key, String(value)),
|
|
removeItem: (key: string) => storage.delete(key),
|
|
clear: () => storage.clear(),
|
|
});
|
|
|
|
FakeIntersectionObserver.instances = [];
|
|
callLogUrls = [];
|
|
deferredDetail = null;
|
|
routerControl.pendingUrl = null;
|
|
routerControl.bumpPageRender = () => {};
|
|
routerControl.replace.mockReset();
|
|
routerControl.replace.mockImplementation((url) => {
|
|
routerControl.pendingUrl = url;
|
|
routerControl.bumpPageRender();
|
|
});
|
|
|
|
vi.stubGlobal("IntersectionObserver", FakeIntersectionObserver);
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
if (url.startsWith("/api/usage/call-logs")) {
|
|
callLogUrls.push(url);
|
|
const limit = Number(new URL(url, "http://test").searchParams.get("limit"));
|
|
return Response.json(LOG_ROWS.slice(0, limit));
|
|
}
|
|
if (url.startsWith("/api/logs/detail")) {
|
|
return Response.json({ enabled: false });
|
|
}
|
|
if (url.startsWith("/api/logs/")) {
|
|
const id = url.split("/api/logs/")[1]?.split("?")[0];
|
|
if (deferredDetail?.id === id) return deferredDetail.promise;
|
|
return Response.json(LOG_ROWS.find((row) => row.id === id));
|
|
}
|
|
if (url.startsWith("/api/provider-nodes")) {
|
|
return Response.json({ nodes: [] });
|
|
}
|
|
return Response.json({});
|
|
})
|
|
);
|
|
|
|
vi.useFakeTimers();
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await act(async () => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
vi.useRealTimers();
|
|
vi.unstubAllGlobals();
|
|
window.history.replaceState(null, "", "/dashboard/logs");
|
|
});
|
|
|
|
describe("request-log position preservation (#9154)", () => {
|
|
it("opens an older row without changing the loaded, filtered, sorted, or scrolled view", { timeout: 30000 }, async () => {
|
|
const scrollContainer = await renderExpandedView();
|
|
|
|
await openOlderRow();
|
|
|
|
expect(routerControl.pendingUrl).toBe("/dashboard/logs?view=requests&tenant=kept&id=log-080");
|
|
expect(routerControl.replace).toHaveBeenLastCalledWith(
|
|
"/dashboard/logs?view=requests&tenant=kept&id=log-080",
|
|
{ scroll: false }
|
|
);
|
|
assertRetainedView(scrollContainer);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"close button",
|
|
async () => {
|
|
container.querySelector<HTMLButtonElement>('[aria-label="Close detail modal"]')!.click();
|
|
},
|
|
],
|
|
[
|
|
"Escape",
|
|
async () => {
|
|
globalThis.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
},
|
|
],
|
|
[
|
|
"backdrop",
|
|
async () => {
|
|
container.querySelector<HTMLElement>('[aria-label="Request log detail"]')!.click();
|
|
},
|
|
],
|
|
])(
|
|
"closes through %s without changing the loaded, filtered, sorted, or scrolled view",
|
|
{ timeout: 30000 },
|
|
async (_name, close) => {
|
|
const scrollContainer = await renderExpandedView();
|
|
await openOlderRow();
|
|
commitPendingUrl();
|
|
routerControl.replace.mockClear();
|
|
|
|
await act(async () => {
|
|
await close();
|
|
});
|
|
await settle();
|
|
|
|
expect(routerControl.pendingUrl).toBe("/dashboard/logs?view=requests&tenant=kept");
|
|
expect(routerControl.replace).toHaveBeenCalledTimes(1);
|
|
expect(routerControl.replace).toHaveBeenCalledWith(
|
|
"/dashboard/logs?view=requests&tenant=kept",
|
|
{ scroll: false }
|
|
);
|
|
commitPendingUrl();
|
|
expect(container.querySelector('[aria-label="Request log detail"]')).toBeNull();
|
|
assertRetainedView(scrollContainer);
|
|
}
|
|
);
|
|
|
|
it("opens a direct id deep link on mount", { timeout: 30000 }, async () => {
|
|
window.history.replaceState(null, "", "/dashboard/logs?tenant=kept&id=log-080");
|
|
|
|
await act(async () => {
|
|
root.render(<Harness />);
|
|
});
|
|
await settle();
|
|
|
|
expect(container.querySelector('[aria-label="Request log detail"]')).not.toBeNull();
|
|
});
|
|
|
|
it("does not reopen a closed modal when its stale detail request completes", { timeout: 30000 }, async () => {
|
|
deferredDetail = createDeferredDetail("log-000");
|
|
window.history.replaceState(null, "", "/dashboard/logs?tenant=kept");
|
|
|
|
await act(async () => {
|
|
root.render(<Harness />);
|
|
});
|
|
await settle();
|
|
|
|
const row = Array.from(container.querySelectorAll<HTMLTableRowElement>("tbody tr")).find(
|
|
(item) => item.textContent?.includes("model-000")
|
|
);
|
|
await act(async () => {
|
|
row!.click();
|
|
});
|
|
expect(container.querySelector('[aria-label="Request log detail"]')).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
container.querySelector<HTMLButtonElement>('[aria-label="Close detail modal"]')!.click();
|
|
});
|
|
expect(container.querySelector('[aria-label="Request log detail"]')).toBeNull();
|
|
|
|
await act(async () => {
|
|
deferredDetail!.resolve(Response.json(LOG_ROWS[0]));
|
|
await deferredDetail!.promise;
|
|
});
|
|
await settle();
|
|
|
|
expect(container.querySelector('[aria-label="Request log detail"]')).toBeNull();
|
|
});
|
|
});
|