Files
OmniRoute/tests/unit/ui/request-logger-position-9154.test.tsx
Diego Rodrigues de Sa e Souza f6ccd3cf9f fix(quality): green release/v3.8.50 base-reds round 2 (#9985) (#10131)
* 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>
2026-08-12 16:09:34 -03:00

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();
});
});