mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
fix(dashboard): derive display base URL from origin instead of hardcoding localhost (#1960)
Integrated into release/v3.7.9
This commit is contained in:
@@ -209,6 +209,14 @@ CLOUD_URL=
|
||||
# Public-facing base URL — CRITICAL for reverse proxy / OAuth callback setups.
|
||||
# Used by: OAuth redirect_uri computation, Dashboard UI links, cloud/model sync.
|
||||
# Set to your public URL when behind nginx/Caddy (e.g., https://omniroute.example.com).
|
||||
#
|
||||
# Dashboard display behavior: when this variable is unset, the dashboard
|
||||
# auto-detects the base URL shown in curl examples and CLI tool snippets
|
||||
# from window.location.origin (the host the user is browsing). Setting it
|
||||
# explicitly is only required when running behind a reverse proxy with a
|
||||
# different public hostname, or when OAuth callbacks must point to a
|
||||
# canonical URL.
|
||||
#
|
||||
# Default: http://localhost:20128
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:20128
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
CustomCliCard,
|
||||
} from "./components";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
const AUTO_CONFIGURED_TOOL_IDS = new Set([
|
||||
@@ -226,7 +227,7 @@ export default function CLIToolsPageClient({ machineId: _machineId }) {
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.origin;
|
||||
}
|
||||
return "http://localhost:20128";
|
||||
return DEFAULT_DISPLAY_BASE_URL;
|
||||
};
|
||||
|
||||
if (loading || !statusesLoaded) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/comp
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -147,7 +148,7 @@ export default function ClineToolCard({
|
||||
|
||||
const getEffectiveBaseUrl = () => {
|
||||
if (customBaseUrl) return customBaseUrl;
|
||||
return baseUrl || "http://localhost:20128";
|
||||
return baseUrl || DEFAULT_DISPLAY_BASE_URL;
|
||||
};
|
||||
|
||||
const handleApply = async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
buildCustomCliJsonConfig,
|
||||
normalizeOpenAiBaseUrl,
|
||||
} from "./customCliConfig";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
@@ -59,7 +60,7 @@ export default function CustomCliCard({
|
||||
(!cloudEnabled
|
||||
? "sk_omniroute"
|
||||
: translateOrFallback("yourApiKeyPlaceholder", "sk-your-omniroute-key"));
|
||||
const baseUrlWithV1 = normalizeOpenAiBaseUrl(baseUrl || "http://localhost:20128");
|
||||
const baseUrlWithV1 = normalizeOpenAiBaseUrl(baseUrl || DEFAULT_DISPLAY_BASE_URL);
|
||||
const chatCompletionsEndpoint = `${baseUrlWithV1}/chat/completions`;
|
||||
|
||||
const envScript = useMemo(
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
|
||||
import { copyToClipboard } from "@/shared/utils/clipboard";
|
||||
import { buildOpenCodeConfigDocument } from "@/shared/services/opencodeConfig";
|
||||
import { useTheme } from "@/shared/hooks/useTheme";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
|
||||
export default function DefaultToolCard({
|
||||
toolId,
|
||||
@@ -90,7 +91,7 @@ export default function DefaultToolCard({
|
||||
[getSelectedModelEntries]
|
||||
);
|
||||
|
||||
const normalizedBaseUrl = baseUrl || "http://localhost:20128";
|
||||
const normalizedBaseUrl = baseUrl || DEFAULT_DISPLAY_BASE_URL;
|
||||
const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1")
|
||||
? normalizedBaseUrl
|
||||
: `${normalizedBaseUrl}/v1`;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/comp
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -133,7 +134,7 @@ export default function KiloToolCard({
|
||||
|
||||
const getEffectiveBaseUrl = () => {
|
||||
if (customBaseUrl) return customBaseUrl;
|
||||
return baseUrl || "http://localhost:20128";
|
||||
return baseUrl || DEFAULT_DISPLAY_BASE_URL;
|
||||
};
|
||||
|
||||
const handleApply = async () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
|
||||
export interface CustomCliAliasMapping {
|
||||
alias: string;
|
||||
model: string;
|
||||
@@ -12,7 +14,7 @@ export interface CustomCliConfigInput {
|
||||
}
|
||||
|
||||
export function normalizeOpenAiBaseUrl(baseUrl: string): string {
|
||||
const trimmed = (baseUrl || "http://localhost:20128").trim().replace(/\/+$/, "");
|
||||
const trimmed = (baseUrl || DEFAULT_DISPLAY_BASE_URL).trim().replace(/\/+$/, "");
|
||||
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
|
||||
/* ─── Types ──────────────────────────────────────────── */
|
||||
interface Endpoint {
|
||||
@@ -65,6 +66,7 @@ const WEBHOOK_EVENTS = [
|
||||
|
||||
/* ─── Main Component ─────────────────────────────────── */
|
||||
export default function ApiEndpointsTab() {
|
||||
const baseUrl = useDisplayBaseUrl();
|
||||
const [catalog, setCatalog] = useState<CatalogData | null>(null);
|
||||
const [catalogError, setCatalogError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -536,7 +538,7 @@ export default function ApiEndpointsTab() {
|
||||
Example
|
||||
</p>
|
||||
<code className="text-[11px] font-mono text-text-main break-all">
|
||||
curl -X {ep.method} http://localhost:20128
|
||||
curl -X {ep.method} {baseUrl}
|
||||
{ep.path.replace("/api/", "/")}
|
||||
{ep.security ? ' -H "Authorization: Bearer YOUR_KEY"' : ""}
|
||||
{ep.requestBody
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, Button, Input, Modal, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -1007,7 +1008,8 @@ export default function APIPageClient({ machineId }: APIPageClientProps) {
|
||||
}
|
||||
}, [fetchTailscaleStatus, handleTailscaleEnable, tailscalePassword, translateOrFallback]);
|
||||
|
||||
const [baseUrl, setBaseUrl] = useState("/v1");
|
||||
const displayBaseUrl = useDisplayBaseUrl();
|
||||
const baseUrl = `${displayBaseUrl}/v1`;
|
||||
const normalizedCloudBaseUrl = cloudBaseUrl
|
||||
? resolvedMachineId && !cloudBaseUrl.endsWith(`/${resolvedMachineId}`)
|
||||
? `${cloudBaseUrl}/${resolvedMachineId}`
|
||||
@@ -1015,14 +1017,6 @@ export default function APIPageClient({ machineId }: APIPageClientProps) {
|
||||
: null;
|
||||
const cloudEndpointNew = normalizedCloudBaseUrl ? `${normalizedCloudBaseUrl}/v1` : null;
|
||||
|
||||
// Hydration fix: Only access window on client side
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const defaultOrigin = process.env.NEXT_PUBLIC_BASE_URL || window.location.origin;
|
||||
setBaseUrl(`${defaultOrigin}/v1`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
|
||||
@@ -104,4 +104,48 @@ describe("ApiEndpointsTab", () => {
|
||||
expect(document.body.textContent).toContain("1 endpoints across 1 categories");
|
||||
expect(document.body.textContent).toContain("/api/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("renders curl example using window.location.origin when NEXT_PUBLIC_BASE_URL is unset", async () => {
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({
|
||||
info: { title: "OmniRoute API", version: "3.7.6" },
|
||||
servers: [],
|
||||
tags: [{ name: "Chat" }],
|
||||
endpoints: [
|
||||
{
|
||||
method: "POST",
|
||||
path: "/api/v1/chat/completions",
|
||||
tags: ["Chat"],
|
||||
summary: "Create chat completion",
|
||||
description: "Create chat completion",
|
||||
security: false,
|
||||
parameters: [],
|
||||
requestBody: false,
|
||||
responses: ["200"],
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
})
|
||||
);
|
||||
|
||||
renderApiEndpointsTab();
|
||||
|
||||
await waitForText("OmniRoute API");
|
||||
|
||||
// Expand the endpoint to reveal the curl example
|
||||
const endpointRow = document.body.querySelector("code.font-mono.flex-1");
|
||||
if (endpointRow?.parentElement) {
|
||||
await act(async () => {
|
||||
endpointRow.parentElement!.click();
|
||||
});
|
||||
}
|
||||
|
||||
// After mount the hook swaps DEFAULT_DISPLAY_BASE_URL for window.location.origin.
|
||||
// In jsdom the default origin is "http://localhost".
|
||||
const expectedOrigin = window.location.origin; // "http://localhost" in jsdom
|
||||
await waitForText(`curl -X POST ${expectedOrigin}/v1/chat/completions`);
|
||||
expect(document.body.textContent).toContain(
|
||||
`curl -X POST ${expectedOrigin}/v1/chat/completions`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
|
||||
const STEP_IDS = ["welcome", "security", "provider", "test", "done"];
|
||||
const STEP_ICONS = ["waving_hand", "lock", "dns", "play_circle", "check_circle"];
|
||||
@@ -20,9 +21,10 @@ export default function OnboardingWizard() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("onboarding");
|
||||
const tc = useTranslations("common");
|
||||
const baseUrl = useDisplayBaseUrl();
|
||||
const [step, setStep] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [apiEndpoint, setApiEndpoint] = useState("http://localhost:20128/api/v1");
|
||||
const [apiEndpoint, setApiEndpoint] = useState(`${baseUrl}/api/v1`);
|
||||
|
||||
// Security step state
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { copyToClipboard } from "@/shared/utils/clipboard";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
|
||||
export default function GetStarted() {
|
||||
const t = useTranslations("landing");
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const endpoint = "http://localhost:20128";
|
||||
const endpoint = useDisplayBaseUrl();
|
||||
const dashboardUrl = `${endpoint}/dashboard`;
|
||||
const command = "npx omniroute";
|
||||
|
||||
|
||||
176
src/shared/hooks/__tests__/useDisplayBaseUrl.test.tsx
Normal file
176
src/shared/hooks/__tests__/useDisplayBaseUrl.test.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "../useDisplayBaseUrl";
|
||||
|
||||
const cleanupCallbacks: Array<() => void> = [];
|
||||
|
||||
function makeContainer(): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
cleanupCallbacks.push(() => {
|
||||
container.remove();
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("useDisplayBaseUrl", () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanupCallbacks.length > 0) {
|
||||
cleanupCallbacks.pop()?.();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("returns env value on first render and after mount when NEXT_PUBLIC_BASE_URL is set", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "https://example.com");
|
||||
|
||||
const { useDisplayBaseUrl } = await import("../useDisplayBaseUrl");
|
||||
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
|
||||
function C() {
|
||||
const url = useDisplayBaseUrl();
|
||||
return <span data-testid="value">{url}</span>;
|
||||
}
|
||||
|
||||
// Synchronous act: commits render and flushes synchronous effects.
|
||||
// The queueMicrotask in useEffect has not yet fired.
|
||||
act(() => {
|
||||
root.render(<C />);
|
||||
});
|
||||
|
||||
// Env set: first render shows env value (useEffect no-ops when envValue is set)
|
||||
expect(container.querySelector('[data-testid="value"]')?.textContent).toBe(
|
||||
"https://example.com"
|
||||
);
|
||||
|
||||
// Flush microtasks and any remaining async work
|
||||
await act(async () => {});
|
||||
|
||||
// Env still wins after mount
|
||||
expect(container.querySelector('[data-testid="value"]')?.textContent).toBe(
|
||||
"https://example.com"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns DEFAULT_DISPLAY_BASE_URL on first render and origin after mount when env unset", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "");
|
||||
|
||||
const { useDisplayBaseUrl } = await import("../useDisplayBaseUrl");
|
||||
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
|
||||
function C() {
|
||||
const url = useDisplayBaseUrl();
|
||||
return <span data-testid="value">{url}</span>;
|
||||
}
|
||||
|
||||
// Synchronous act commits render. useEffect fires but queueMicrotask
|
||||
// schedules setState for after this act() call returns.
|
||||
act(() => {
|
||||
root.render(<C />);
|
||||
});
|
||||
|
||||
// Pre-microtask: DOM still shows the initial state (DEFAULT_DISPLAY_BASE_URL)
|
||||
expect(container.querySelector('[data-testid="value"]')?.textContent).toBe(
|
||||
DEFAULT_DISPLAY_BASE_URL
|
||||
);
|
||||
|
||||
// Flush queueMicrotask callback (setState) and resulting re-render
|
||||
await act(async () => {});
|
||||
|
||||
// After mount: swaps to window.location.origin
|
||||
expect(container.querySelector('[data-testid="value"]')?.textContent).toBe(
|
||||
window.location.origin
|
||||
);
|
||||
});
|
||||
|
||||
it("trims and strips trailing slash from env value", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_BASE_URL", " https://x.com/ ");
|
||||
|
||||
const { useDisplayBaseUrl } = await import("../useDisplayBaseUrl");
|
||||
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
|
||||
function C() {
|
||||
const url = useDisplayBaseUrl();
|
||||
return <span data-testid="value">{url}</span>;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
root.render(<C />);
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="value"]')?.textContent).toBe("https://x.com");
|
||||
});
|
||||
|
||||
it("strips trailing slash from window.location.origin after mount", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "");
|
||||
|
||||
// Stub window.location with trailing slash on origin
|
||||
vi.stubGlobal("location", { origin: "http://192.168.13.62:20128/" });
|
||||
|
||||
const { useDisplayBaseUrl } = await import("../useDisplayBaseUrl");
|
||||
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
|
||||
function C() {
|
||||
const url = useDisplayBaseUrl();
|
||||
return <span data-testid="value">{url}</span>;
|
||||
}
|
||||
|
||||
// Render and flush all effects including queueMicrotask
|
||||
await act(async () => {
|
||||
root.render(<C />);
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="value"]')?.textContent).toBe(
|
||||
"http://192.168.13.62:20128"
|
||||
);
|
||||
});
|
||||
|
||||
it("treats empty-string env as unset and falls through to origin after mount", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "");
|
||||
|
||||
const { useDisplayBaseUrl } = await import("../useDisplayBaseUrl");
|
||||
|
||||
const container = makeContainer();
|
||||
const root = createRoot(container);
|
||||
|
||||
function C() {
|
||||
const url = useDisplayBaseUrl();
|
||||
return <span data-testid="value">{url}</span>;
|
||||
}
|
||||
|
||||
// Synchronous act: render committed, useEffect fired, microtask queued but not yet run
|
||||
act(() => {
|
||||
root.render(<C />);
|
||||
});
|
||||
|
||||
// Empty env treated as unset → initial state is DEFAULT_DISPLAY_BASE_URL
|
||||
expect(container.querySelector('[data-testid="value"]')?.textContent).toBe(
|
||||
DEFAULT_DISPLAY_BASE_URL
|
||||
);
|
||||
|
||||
// Flush queueMicrotask + re-render
|
||||
await act(async () => {});
|
||||
|
||||
// After mount: resolves to origin
|
||||
const result = container.querySelector('[data-testid="value"]')?.textContent;
|
||||
expect(result).toBe(window.location.origin.replace(/\/+$/, ""));
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
// Shared Hooks - Export all
|
||||
export { useTheme } from "./useTheme";
|
||||
export { useDisplayBaseUrl, DEFAULT_DISPLAY_BASE_URL } from "./useDisplayBaseUrl";
|
||||
|
||||
49
src/shared/hooks/useDisplayBaseUrl.ts
Normal file
49
src/shared/hooks/useDisplayBaseUrl.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const DEFAULT_DISPLAY_BASE_URL = "http://localhost:20128";
|
||||
|
||||
function normalizeUrl(value?: string): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return null;
|
||||
return trimmed.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public base URL to display in the dashboard.
|
||||
*
|
||||
* Resolution chain:
|
||||
* 1. NEXT_PUBLIC_BASE_URL (env, trimmed + slash-normalized) — wins if set.
|
||||
* 2. window.location.origin after client mount — when env is unset.
|
||||
* 3. DEFAULT_DISPLAY_BASE_URL ("http://localhost:20128") — SSR / first render fallback.
|
||||
*
|
||||
* DISPLAY ONLY — do NOT use this hook for OAuth `redirect_uri`.
|
||||
* OAuth callers must read `process.env.NEXT_PUBLIC_BASE_URL` directly to avoid
|
||||
* host-header attack surface. For server-side resolution, use
|
||||
* `src/shared/utils/resolveOmniRouteBaseUrl.ts` instead.
|
||||
*/
|
||||
export function useDisplayBaseUrl(): string {
|
||||
const envValue = normalizeUrl(process.env.NEXT_PUBLIC_BASE_URL);
|
||||
|
||||
const [url, setUrl] = useState<string>(envValue ?? DEFAULT_DISPLAY_BASE_URL);
|
||||
|
||||
useEffect(() => {
|
||||
if (envValue) return;
|
||||
const origin = normalizeUrl(window.location.origin) ?? DEFAULT_DISPLAY_BASE_URL;
|
||||
// Schedule via queueMicrotask so setState is called inside a callback,
|
||||
// not synchronously in the effect body (react-hooks/set-state-in-effect).
|
||||
// The unmounted guard prevents a stale setState on a torn-down root
|
||||
// (relevant under React strict mode's double-invoke, where cleanup runs
|
||||
// before the microtask fires on the first effect invocation).
|
||||
let unmounted = false;
|
||||
queueMicrotask(() => {
|
||||
if (!unmounted) setUrl(origin);
|
||||
});
|
||||
return () => {
|
||||
unmounted = true;
|
||||
};
|
||||
}, [envValue]);
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export default defineConfig({
|
||||
include: [
|
||||
"src/app/**/dashboard/cache/__tests__/**/*.test.tsx",
|
||||
"src/app/**/dashboard/endpoint/__tests__/**/*.test.tsx",
|
||||
"src/shared/hooks/__tests__/**/*.test.tsx",
|
||||
"src/lib/memory/__tests__/**/*.test.ts",
|
||||
"src/lib/skills/__tests__/**/*.test.ts",
|
||||
"tests/unit/encryption.test.ts",
|
||||
|
||||
Reference in New Issue
Block a user