Files
OmniRoute/tests/unit/ui/use-presets.test.tsx
Diego Rodrigues de Sa e Souza af65171e3f fix(ci): clear the base-reds the afternoon merge batch left on release/v3.8.51 (round 5: provider count 352, TS2554/TS2677) (#12144)
* fix(ci): clear the base-reds the 2026-08-30 afternoon merge batch left on release/v3.8.51 (round 5)

- docs-counts / check-docs-counts-sync test: #12103 (Perplexity Agent) made it 352
  providers; README, AGENTS.md, llm.txt (+42 i18n mirrors), package.json description
  and the 4 README diagrams still said 351.
- api-route-typecheck: #11971 passes a third `{ featureEnabled }` argument to
  appendNoThinkingVariants() that the helper never accepted (TS2554 — and the flag
  silently did nothing); the helper now honours it. src/lib/skills/interception.ts
  narrowed a mapped object with a `Record<string, string>` predicate (TS2677) —
  predicate typed with the actual element shape.

Gates: check:docs-counts OK (test 28/28), check:docs-sync PASS, check:api-typecheck
OK (289 frozen). Refs #12103, #11971

* docs(env): document RATE_LIMIT_EXECUTION_MAX_WAIT_MS (#12027 added it to .env.example only)

* fix(ci): round 5b — freeze the react-hooks compiler-rule violations, align 7 tests to merged contracts

No new ESLint warnings: the exact CI command (lint:json --max-warnings 0) reports 278
problems on the tip — 226 from eslint-plugin-react-hooks 7 compiler rules
(set-state-in-effect 167, immutability 36, refs/static-components/purity/
preserve-manual-memoization) that were masked until the lockfile change of
dfc84ba030 invalidated the ESLint cache, plus 46 no-explicit-any in
tests/unit/call-log-cap.test.ts (#12026). Velocity phase: frozen with
`eslint --suppress-all` (+668 suppressions); the 5 now-unused
`eslint-disable react-hooks/immutability` directives and one unused import removed.
Verified: lint:json --max-warnings 0 → 0 problems.

Tests aligned to contracts merged this afternoon (all reproduced red on the pure tip):
- providers-constants-split: 235 → 236 (Perplexity Agent, #12103)
- sse-auth: a forced pin outside allowedConnections now yields no credential
  instead of silently falling back (#12080)
- with-chat-admission-10786: withInjectionGuard(postHandler, { logger: null }) (#12117)
- hard-session-lease-bypass-inventory: classify src/app/api/oauth/codex/import/route.ts (#12116)
- usage-service-hardening: OpenCode Go official usage API shape (#12124)
- i18n placeholder parity: apiManager.restrictedToConnections rewritten as a plain
  ICU plural (`{count, plural, one {# connection} other {# connections}}`) in en,
  vi, pt-BR and the 40 __MISSING__ mirrors — the parity extractor counts every
  `{word}` including the old literal `{s}`

Refs #12103, #12080, #12117, #12116, #12124, #12026

* fix(ci): run the ESLint warnings job on the box with an 8 GB heap; reserved-prefix set 398 → 400

The cold full lint with the react-hooks 7 compiler rules is killed on the 7 GB hosted
runner with no message (status null → exit 1, JSON never written) — it only looked
green while the ESLint cache was warm. tests/unit/provider-node-reserved-prefix.test.ts
aligned to the two prefixes the afternoon batch registered (#12103).

* test(ci): document the lint-guard runner exception; #9147 event-loop gap 400 → 800 ms

quality-rail-gate-membership pinned lint-guard to ubuntu-latest; the cold full lint is
OOM-killed there, so the job now runs on omni-light with an 8 GB heap — the test keeps
fast-gates pinned and asserts the documented exception. With the catalog at 352
providers the hosted shards measure 410–633 ms gaps on 9147-catalog-eventloop-yield
(3 runs); 800 ms still fails a true pin. Re-tighten with the v4.0 catalog split.

* chore(quality): summarize the ESLint report on failure — a red lint:json printed nothing

--format json --output-file swallows every problem; a red 'No new ESLint warnings' job
gave zero output (three blind debugging rounds in #12144), and a killed process (OOM,
status null) was equally silent. On any non-zero exit the runner now prints the problem
count and the first 60 'file:line rule — message' lines from the report.

* chore(lint): freeze react-hooks/immutability for the 5 UI test harnesses in the suppressions file

The rule fires for these files in CI but not locally (compiler analysis divergence),
so the inline eslint-disable directives read as 'unused directive' warnings locally.
A suppressions entry is symmetric: suppressed where the rule fires, tolerated as
unpruned (--pass-on-unpruned-suppressions) where it does not. Found via the new
lint:json failure summary.
2026-08-30 18:03:47 -03:00

288 lines
8.9 KiB
TypeScript

// @vitest-environment jsdom
// tests/unit/ui/use-presets.test.tsx
// Runs via Vitest (vitest.config.ts)
// Uses React DOM directly (no @testing-library/dom dep required).
import React, { act, useRef } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
usePresets,
} from "../../../src/app/(dashboard)/dashboard/playground/hooks/usePresets";
import type { PlaygroundPresetListItem } from "../../../src/shared/schemas/playground";
// ─── Minimal hook test harness ────────────────────────────────────────────────
type HookResult<T> = { current: T };
function mountHook<T>(useHook: () => T): {
hookRef: HookResult<T>;
unmount: () => void;
} {
const hookRef: HookResult<T> = { current: undefined as unknown as T };
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
function HookComponent() {
const captureRef = useRef<T>(undefined as unknown as T);
captureRef.current = useHook();
hookRef.current = captureRef.current;
return null;
}
act(() => {
root.render(React.createElement(HookComponent));
});
return {
hookRef,
unmount: () => {
act(() => root.unmount());
container.remove();
},
};
}
// ─── Test fixtures ────────────────────────────────────────────────────────────
const MOCK_PRESET: PlaygroundPresetListItem = {
id: "550e8400-e29b-41d4-a716-446655440000",
name: "Test Preset",
endpoint: "chat.completions",
model: "gpt-4o",
system: "You are helpful.",
params: { temperature: 0.7 },
created_at: "2026-01-01T00:00:00.000Z",
};
function mockFetchOnce(response: unknown, status = 200): ReturnType<typeof vi.fn> {
return vi.fn().mockResolvedValue({
ok: status >= 200 && status < 300,
status,
json: async () => response,
});
}
// ─── Tests ────────────────────────────────────────────────────────────────────
describe("usePresets", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("list()", () => {
it("fetches GET /api/playground/presets and populates presets", async () => {
const mockFetch = mockFetchOnce({ presets: [MOCK_PRESET] });
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
await act(async () => {
await result.current.list();
});
expect(mockFetch).toHaveBeenCalledWith("/api/playground/presets");
expect(result.current.presets).toHaveLength(1);
expect(result.current.presets[0].id).toBe(MOCK_PRESET.id);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
unmount();
});
it("sets error when fetch returns non-ok", async () => {
const mockFetch = mockFetchOnce({ error: { message: "Unauthorized" } }, 401);
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
await act(async () => {
await result.current.list();
});
expect(result.current.error).toBe("Unauthorized");
expect(result.current.presets).toHaveLength(0);
unmount();
});
it("sets error when fetch throws a network error", async () => {
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error")));
const { hookRef: result, unmount } = mountHook(() => usePresets());
await act(async () => {
await result.current.list();
});
expect(result.current.error).toBe("Network error");
unmount();
});
});
describe("create()", () => {
it("calls POST /api/playground/presets with correct body", async () => {
const mockFetch = vi
.fn()
.mockResolvedValueOnce({
ok: true,
status: 201,
json: async () => MOCK_PRESET,
})
// list() is called after create
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ presets: [MOCK_PRESET] }),
});
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
const input = {
name: "Test Preset",
endpoint: "chat.completions",
model: "gpt-4o",
system: "You are helpful.",
params: { temperature: 0.7 },
};
let created: PlaygroundPresetListItem | null = null;
await act(async () => {
created = await result.current.create(input);
});
// First call: POST to presets
expect(mockFetch).toHaveBeenNthCalledWith(
1,
"/api/playground/presets",
expect.objectContaining({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
}),
);
// Second call: GET list to refresh
expect(mockFetch).toHaveBeenNthCalledWith(2, "/api/playground/presets");
expect(created).not.toBeNull();
expect((created as PlaygroundPresetListItem | null)?.id).toBe(MOCK_PRESET.id);
unmount();
});
it("returns null and sets error on failure", async () => {
const mockFetch = mockFetchOnce({ error: { message: "Bad request" } }, 400);
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
let created: PlaygroundPresetListItem | null = null;
await act(async () => {
created = await result.current.create({
name: "x",
endpoint: "chat.completions",
model: "gpt-4o",
});
});
expect(created).toBeNull();
expect(result.current.error).toBe("Bad request");
unmount();
});
});
describe("update()", () => {
it("calls PUT /api/playground/presets/:id with correct body", async () => {
const updated = { ...MOCK_PRESET, name: "Updated" };
const mockFetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, status: 200, json: async () => updated })
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ presets: [updated] }),
});
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
const patch = { name: "Updated" };
let res: PlaygroundPresetListItem | null = null;
await act(async () => {
res = await result.current.update(MOCK_PRESET.id, patch);
});
expect(mockFetch).toHaveBeenNthCalledWith(
1,
`/api/playground/presets/${MOCK_PRESET.id}`,
expect.objectContaining({
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
}),
);
expect((res as PlaygroundPresetListItem | null)?.name).toBe("Updated");
unmount();
});
});
describe("remove()", () => {
it("calls DELETE /api/playground/presets/:id", async () => {
const mockFetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, status: 204, json: async () => ({}) })
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ presets: [] }),
});
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
await act(async () => {
await result.current.remove(MOCK_PRESET.id);
});
expect(mockFetch).toHaveBeenNthCalledWith(
1,
`/api/playground/presets/${MOCK_PRESET.id}`,
expect.objectContaining({ method: "DELETE" }),
);
// After remove, list is refetched => presets = []
expect(result.current.presets).toHaveLength(0);
unmount();
});
it("sets error on failure", async () => {
const mockFetch = mockFetchOnce({ error: { message: "Not found" } }, 404);
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
await act(async () => {
await result.current.remove("nonexistent-id");
});
expect(result.current.error).toBe("Not found");
unmount();
});
});
describe("loading state", () => {
it("loading is false after list() completes", async () => {
const mockFetch = mockFetchOnce({ presets: [] });
vi.stubGlobal("fetch", mockFetch);
const { hookRef: result, unmount } = mountHook(() => usePresets());
await act(async () => {
await result.current.list();
});
expect(result.current.loading).toBe(false);
unmount();
});
});
});