test(ui): cover quota-share page, components, hooks, plans config + sidebar (B/F9)

This commit is contained in:
diegosouzapw
2026-05-28 08:25:05 -03:00
parent c21bfd5a36
commit a4ee78322f
7 changed files with 804 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
const sidebarVisibility = await import("../../src/shared/constants/sidebarVisibility.ts");
function sectionItems(sectionId: string) {
const section = sidebarVisibility.SIDEBAR_SECTIONS.find((s) => s.id === sectionId);
assert.ok(section, `expected section "${sectionId}" to exist`);
return sidebarVisibility.getSectionItems(section);
}
test("HIDEABLE_SIDEBAR_ITEM_IDS contains costs-quota-plans", () => {
assert.ok(
(sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("costs-quota-plans"),
"costs-quota-plans must be in HIDEABLE_SIDEBAR_ITEM_IDS"
);
});
test("HIDEABLE_SIDEBAR_ITEM_IDS: costs-quota-plans appears after costs-quota-share", () => {
const ids = sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[];
const qsIdx = ids.indexOf("costs-quota-share");
const qpIdx = ids.indexOf("costs-quota-plans");
assert.ok(qsIdx !== -1, "costs-quota-share must exist");
assert.ok(qpIdx !== -1, "costs-quota-plans must exist");
assert.ok(qpIdx > qsIdx, "costs-quota-plans must come after costs-quota-share");
});
test("costs section has 5 items including costs-quota-plans at end", () => {
const items = sectionItems("costs");
const ids = items.map((i) => i.id);
assert.ok(ids.includes("costs-quota-plans"), "costs section must include costs-quota-plans");
assert.strictEqual(ids[ids.length - 1], "costs-quota-plans", "costs-quota-plans must be last");
assert.strictEqual(ids.length, 5, "costs section must have exactly 5 items");
});
test("costs-quota-plans has correct href and icon", () => {
const items = sectionItems("costs");
const item = items.find((i) => i.id === "costs-quota-plans");
assert.ok(item, "costs-quota-plans item must exist");
assert.strictEqual(item.href, "/dashboard/costs/quota-share/plans");
assert.strictEqual(item.icon, "fact_check");
assert.strictEqual(item.i18nKey, "costsQuotaPlans");
assert.strictEqual(item.subtitleKey, "costsQuotaPlansSubtitle");
});

View File

@@ -0,0 +1,89 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: AllocationTable } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/AllocationTable"
);
const ALLOCATIONS = [
{ apiKeyId: "key_1", weight: 60, policy: "hard" as const },
{ apiKeyId: "key_2", weight: 40, policy: "soft" as const },
];
const KEY_LABELS: Record<string, string> = { key_1: "KeyOne", key_2: "KeyTwo" };
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function render(props: Parameters<typeof AllocationTable>[0]) {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<AllocationTable {...props} />);
});
}
describe("AllocationTable", { timeout: 10000 }, () => {
afterEach(() => {
if (root && container) act(() => root!.unmount());
container?.remove();
container = null;
root = null;
});
it("renders empty state when no allocations", async () => {
await render({ allocations: [], usage: null, keyLabels: {} });
expect(document.body.innerHTML).toContain("noAllocations");
});
it("renders key labels", async () => {
await render({ allocations: ALLOCATIONS, usage: null, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("KeyOne");
expect(document.body.innerHTML).toContain("KeyTwo");
});
it("renders weights correctly", async () => {
await render({ allocations: ALLOCATIONS, usage: null, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("60%");
expect(document.body.innerHTML).toContain("40%");
});
it("renders policy badges", async () => {
await render({ allocations: ALLOCATIONS, usage: null, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("hard");
expect(document.body.innerHTML).toContain("soft");
});
it("renders consumed values from usage perKey data", async () => {
const usage = {
dimensions: [
{
unit: "tokens",
window: "daily",
limit: 1000,
consumedTotal: 400,
perKey: [
{ apiKeyId: "key_1", consumed: 300, fairShare: 600, deficit: -300, borrowing: false },
{ apiKeyId: "key_2", consumed: 100, fairShare: 400, deficit: 300, borrowing: true },
],
},
],
burnRate: null,
};
await render({ allocations: ALLOCATIONS, usage: usage as never, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("300");
expect(document.body.innerHTML).toContain("100");
// borrowing indicator for key_2
expect(document.body.innerHTML).toContain("borrowingIndicator");
});
});

View File

@@ -0,0 +1,70 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// Stub next/dynamic — returns null component (recharts not needed in tests)
vi.mock("next/dynamic", () => ({
default: () => () => null,
}));
const { default: BurnRateChart } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/BurnRateChart"
);
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function render(props: Parameters<typeof BurnRateChart>[0]) {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<BurnRateChart {...props} />);
});
}
describe("BurnRateChart", { timeout: 10000 }, () => {
afterEach(() => {
if (root && container) act(() => root!.unmount());
container?.remove();
container = null;
root = null;
});
it("renders no-data state when usage is null", async () => {
await render({ usage: null });
expect(document.body.innerHTML).toContain("burnRateTitle");
expect(document.body.innerHTML).toContain("no data");
});
it("renders no-data state when burnRate is falsy", async () => {
const usage = {
dimensions: [],
burnRate: null,
};
await render({ usage: usage as never });
expect(document.body.innerHTML).toContain("no data");
});
it("renders chart when usage has burnRate data", async () => {
const usage = {
dimensions: [
{ unit: "tokens", window: "daily", limit: 100000, consumedTotal: 30000, perKey: [] },
],
burnRate: { tokensPerSecond: 10, timeToExhaustionMs: 7_000_000 },
};
await render({ usage: usage as never });
// Should not show no-data message
expect(document.body.innerHTML).not.toContain("no data yet");
// Should show exhaustion label
expect(document.body.innerHTML).toContain("burnRateExhaustsIn");
});
});

View File

@@ -0,0 +1,141 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("next/dynamic", () => ({
default: () => () => null,
}));
vi.mock("@/shared/components/Card", () => ({
default: ({ children }: { children: React.ReactNode }) => <div data-testid="card">{children}</div>,
}));
vi.mock("@/shared/components/ProviderIcon", () => ({
default: ({ providerId }: { providerId: string }) => (
<span data-testid={`provider-icon-${providerId}`} />
),
}));
// Stub sub-components
vi.mock(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/DimensionBar",
() => ({ default: ({ dimension }: { dimension: { unit: string } }) => <div data-testid="dim-bar">{dimension.unit}</div> })
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/AllocationTable",
() => ({ default: () => <div data-testid="alloc-table" /> })
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/BurnRateChart",
() => ({ default: () => <div data-testid="burn-rate-chart" /> })
);
const { default: PoolCard } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/PoolCard"
);
const MOCK_POOL = {
id: "pool_1",
connectionId: "conn_1",
name: "Test Pool",
createdAt: new Date().toISOString(),
allocations: [{ apiKeyId: "key_1", weight: 60, policy: "hard" as const }],
};
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function renderCard(usage = null as null | object) {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(
<PoolCard
pool={MOCK_POOL}
usage={usage as never}
keyLabels={{ key_1: "MyKey" }}
connectionLabel="My Conn"
provider="openai"
onEdit={vi.fn()}
onRemove={vi.fn()}
/>
);
});
}
describe("PoolCard", { timeout: 10000 }, () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
if (root && container) {
act(() => root!.unmount());
}
container?.remove();
container = null;
root = null;
});
it("renders pool name and connection label", async () => {
await renderCard();
expect(document.body.innerHTML).toContain("Test Pool");
expect(document.body.innerHTML).toContain("My Conn");
});
it("renders AllocationTable", async () => {
await renderCard();
expect(document.querySelector("[data-testid='alloc-table']")).not.toBeNull();
});
it("renders DimensionBar when usage has dimensions", async () => {
const usage = {
dimensions: [{ unit: "tokens", window: "daily", limit: 1000, consumedTotal: 500, perKey: [] }],
burnRate: null,
};
await renderCard(usage);
expect(document.querySelector("[data-testid='dim-bar']")).not.toBeNull();
});
it("renders BurnRateChart when usage is non-null", async () => {
const usage = {
dimensions: [{ unit: "tokens", window: "daily", limit: 1000, consumedTotal: 200, perKey: [] }],
burnRate: null,
};
await renderCard(usage);
expect(document.querySelector("[data-testid='burn-rate-chart']")).not.toBeNull();
});
it("calls onEdit when edit button clicked", async () => {
const onEdit = vi.fn();
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(
<PoolCard
pool={MOCK_POOL}
usage={null}
keyLabels={{}}
connectionLabel="label"
provider="openai"
onEdit={onEdit}
onRemove={vi.fn()}
/>
);
});
const editBtn = document.querySelector("button[title='editAllocations']") as HTMLButtonElement;
expect(editBtn).not.toBeNull();
await act(async () => editBtn.click());
expect(onEdit).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,134 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("@/shared/components", () => ({
Button: ({
children,
onClick,
disabled,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}) => (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
),
}));
vi.mock("@/shared/components/ProviderIcon", () => ({
default: () => <span />,
}));
vi.mock("@/lib/quota/planRegistry", () => ({
knownProviders: () => ["openai", "anthropic"],
getKnownPlan: (prov: string) => {
if (prov === "openai") {
return { dimensions: [{ unit: "tokens", window: "daily", limit: 100000 }] };
}
return null;
},
}));
const MOCK_CONNECTIONS = [
{ id: "conn_1", provider: "openai", name: "GPT Account" },
{ id: "conn_2", provider: "anthropic", email: "user@example.com" },
];
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
const { default: ProviderPlanConfigClient } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient"
);
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function renderPage() {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<ProviderPlanConfigClient />);
});
// Wait for initial fetch effect to resolve
await act(async () => {
await new Promise((r) => setTimeout(r, 30));
});
}
describe("ProviderPlanConfigClient", { timeout: 15000 }, () => {
beforeEach(() => {
mockFetch.mockImplementation((url: string) => {
if (String(url).includes("/api/providers/client")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ connections: MOCK_CONNECTIONS }),
} as unknown as Response);
}
if (String(url).includes("/api/quota/plans")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve([]),
} as unknown as Response);
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
} as unknown as Response);
});
});
afterEach(() => {
if (root && container) act(() => root!.unmount());
container?.remove();
container = null;
root = null;
vi.clearAllMocks();
});
it("renders the page title", async () => {
await renderPage();
expect(document.body.innerHTML).toContain("title");
});
it("renders catalog section with known providers", async () => {
await renderPage();
// catalogTitle key should appear
expect(document.body.innerHTML).toContain("catalogTitle");
expect(document.body.innerHTML).toContain("openai");
});
it("renders connection selector with options", async () => {
await renderPage();
const select = document.querySelector("select") as HTMLSelectElement;
expect(select).not.toBeNull();
expect(select.options.length).toBeGreaterThan(1);
});
it("shows right-panel placeholder when no connection selected", async () => {
await renderPage();
expect(document.body.innerHTML).toContain("unknownProviderNotice");
});
it("renders save button after selecting a connection", async () => {
await renderPage();
const select = document.querySelector("select") as HTMLSelectElement;
await act(async () => {
select.value = "conn_1";
select.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(document.body.innerHTML).toContain("saveOverrideButton");
});
});

View File

@@ -0,0 +1,169 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── i18n stub ──────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// ── next/dynamic stub ──────────────────────────────────────────────────────
vi.mock("next/dynamic", () => ({
default: () => () => null,
}));
// ── Shared component stubs ─────────────────────────────────────────────────
vi.mock("@/shared/components", () => ({
Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button onClick={onClick}>{children}</button>
),
Modal: ({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) =>
isOpen ? <div data-testid="modal">{children}</div> : null,
}));
vi.mock("@/shared/components/Card", () => ({
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock("@/shared/components/ProviderIcon", () => ({
default: () => <span />,
}));
// ── Pool data ──────────────────────────────────────────────────────────────
const MOCK_POOLS = [
{
id: "pool_1",
connectionId: "conn_1",
name: "Pool A",
createdAt: new Date().toISOString(),
allocations: [{ apiKeyId: "key_1", weight: 50, policy: "hard" }],
},
{
id: "pool_2",
connectionId: "conn_2",
name: "Pool B",
createdAt: new Date().toISOString(),
allocations: [],
},
];
// ── usePools mock ──────────────────────────────────────────────────────────
const mockMutate = vi.fn().mockResolvedValue(undefined);
const mockUsePools = vi.fn(() => ({
pools: MOCK_POOLS,
loading: false,
error: null,
mutate: mockMutate,
}));
vi.mock(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/hooks/usePools",
() => ({ usePools: mockUsePools })
);
// ── usePoolUsage mock ──────────────────────────────────────────────────────
vi.mock(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/hooks/usePoolUsage",
() => ({
usePoolUsage: () => ({ usage: null, loading: false, error: null }),
})
);
// ── useLocalStoragePoolMigration mock ──────────────────────────────────────
const mockMigration = vi.fn();
vi.mock(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/hooks/useLocalStoragePoolMigration",
() => ({ useLocalStoragePoolMigration: mockMigration })
);
// ── fetch stub ─────────────────────────────────────────────────────────────
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve([]) } as unknown as Response)
)
);
// ── Lazy import after mocks ────────────────────────────────────────────────
const { default: QuotaSharePageClient } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/QuotaSharePageClient"
);
// ── Helpers ───────────────────────────────────────────────────────────────
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function renderComponent() {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<QuotaSharePageClient />);
});
}
async function waitFor(fn: () => boolean, timeout = 3000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeout) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
// ── Tests ─────────────────────────────────────────────────────────────────
describe("QuotaSharePageClient", { timeout: 15000 }, () => {
beforeEach(() => {
mockMigration.mockReset();
vi.clearAllMocks();
mockUsePools.mockReturnValue({
pools: MOCK_POOLS,
loading: false,
error: null,
mutate: mockMutate,
});
});
afterEach(() => {
if (root && container) {
act(() => {
root!.unmount();
});
}
container?.remove();
container = null;
root = null;
});
it("renders 2 PoolCard components when usePools returns 2 pools", async () => {
await renderComponent();
await waitFor(() => {
// Each PoolCard renders the pool name text
return document.body.innerHTML.includes("Pool A");
});
expect(document.body.innerHTML).toContain("Pool A");
expect(document.body.innerHTML).toContain("Pool B");
});
it("renders empty state when pools is empty", async () => {
mockUsePools.mockReturnValue({ pools: [], loading: false, error: null, mutate: mockMutate });
await renderComponent();
await waitFor(() => document.body.innerHTML.includes("emptyTitle"));
expect(document.body.innerHTML).toContain("emptyTitle");
});
it("calls useLocalStoragePoolMigration on mount", async () => {
await renderComponent();
expect(mockMigration).toHaveBeenCalled();
});
it("does not contain localStorage references in rendered output", async () => {
await renderComponent();
expect(document.body.innerHTML).not.toContain("localStorage");
expect(document.body.innerHTML).not.toContain("betaPreviewLabel");
});
});

View File

@@ -0,0 +1,157 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
adaptLsPoolToApiSchema,
useLocalStoragePoolMigration,
} = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/hooks/useLocalStoragePoolMigration"
);
// ── Unit tests for adaptLsPoolToApiSchema ─────────────────────────────────
describe("adaptLsPoolToApiSchema", () => {
it("maps connectionId, accountLabel, and allocations", () => {
const lsPool = {
id: "old_1",
connectionId: "conn_abc",
accountLabel: "My Account",
policy: "soft" as const,
allocations: [{ apiKeyId: "k1", percent: 70 }, { apiKeyId: "k2", percent: 30 }],
};
const result = adaptLsPoolToApiSchema(lsPool);
expect(result.connectionId).toBe("conn_abc");
expect(result.name).toBe("My Account");
expect(result.allocations).toHaveLength(2);
expect(result.allocations[0].weight).toBe(70);
expect(result.allocations[0].policy).toBe("soft");
});
it("defaults policy to hard for unknown policy values", () => {
const lsPool = { connectionId: "c1", policy: "invalid", allocations: [] };
const result = adaptLsPoolToApiSchema(lsPool);
expect(result.allocations).toHaveLength(0);
});
it("filters allocations without apiKeyId", () => {
const lsPool = {
connectionId: "c1",
allocations: [{ apiKeyId: "k1", percent: 100 }, { percent: 50 }],
};
const result = adaptLsPoolToApiSchema(lsPool);
expect(result.allocations).toHaveLength(1);
expect(result.allocations[0].apiKeyId).toBe("k1");
});
it("clamps weight to 0-100", () => {
const lsPool = {
connectionId: "c1",
allocations: [{ apiKeyId: "k1", percent: 150 }, { apiKeyId: "k2", percent: -10 }],
};
const result = adaptLsPoolToApiSchema(lsPool);
expect(result.allocations[0].weight).toBe(100);
expect(result.allocations[1].weight).toBe(0);
});
it("uses provider as fallback name", () => {
const lsPool = { connectionId: "conn_xyz", provider: "openai", allocations: [] };
const result = adaptLsPoolToApiSchema(lsPool);
expect(result.name).toBe("openai");
});
});
// ── Integration tests for useLocalStoragePoolMigration hook ───────────────
const LS_KEY = "omniroute:quota-share:pools";
function HookWrapper({
pools,
mutate,
}: {
pools: object[];
mutate: () => Promise<unknown>;
}) {
useLocalStoragePoolMigration({ pools: pools as never, mutate });
return <div />;
}
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function renderHook(props: Parameters<typeof HookWrapper>[0]) {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<HookWrapper {...props} />);
});
}
describe("useLocalStoragePoolMigration", { timeout: 10000 }, () => {
const mockMutate = vi.fn().mockResolvedValue(undefined);
let fetchSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
localStorage.clear();
fetchSpy = vi.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve({}) } as unknown as Response)
);
vi.stubGlobal("fetch", fetchSpy);
mockMutate.mockClear();
});
afterEach(() => {
if (root && container) act(() => root!.unmount());
container?.remove();
container = null;
root = null;
vi.unstubAllGlobals();
localStorage.clear();
});
it("does nothing when localStorage key is absent", async () => {
await renderHook({ pools: [], mutate: mockMutate });
expect(fetchSpy).not.toHaveBeenCalled();
});
it("does not migrate when DB already has pools (idempotency)", async () => {
const lsPools = [{ connectionId: "c1", allocations: [] }];
localStorage.setItem(LS_KEY, JSON.stringify(lsPools));
const existingPools = [{ id: "p1", connectionId: "c1", name: "Existing", allocations: [] }];
await renderHook({ pools: existingPools, mutate: mockMutate });
// fetch not called — pools already exist
expect(fetchSpy).not.toHaveBeenCalled();
// localStorage key preserved for user safety
expect(localStorage.getItem(LS_KEY)).not.toBeNull();
});
it("migrates LS pools to API when DB is empty", async () => {
const lsPools = [
{ connectionId: "c1", accountLabel: "Acme", policy: "hard", allocations: [{ apiKeyId: "k1", percent: 100 }] },
];
localStorage.setItem(LS_KEY, JSON.stringify(lsPools));
await renderHook({ pools: [], mutate: mockMutate });
// Small tick to let the Promise chain resolve
await act(async () => {
await new Promise((r) => setTimeout(r, 50));
});
expect(fetchSpy).toHaveBeenCalledWith(
"/api/quota/pools",
expect.objectContaining({ method: "POST" })
);
expect(localStorage.getItem(LS_KEY)).toBeNull();
expect(mockMutate).toHaveBeenCalled();
});
it("clears invalid JSON from localStorage", async () => {
localStorage.setItem(LS_KEY, "{invalid}");
await renderHook({ pools: [], mutate: mockMutate });
expect(fetchSpy).not.toHaveBeenCalled();
expect(localStorage.getItem(LS_KEY)).toBeNull();
});
});