import { act, renderHook, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AUTH_TOKEN_KEY } from "@/lib/auth/client";
import { useModels } from "@/hooks/use-models";
let storage: Storage;
beforeEach(() => {
const values = new Map<string, string>();
storage = {
get length() {
return values.size;
},
clear: () => values.clear(),
getItem: (key) => values.get(key) ?? null,
key: (index) => [...values.keys()][index] ?? null,
removeItem: (key) => values.delete(key),
setItem: (key, value) => values.set(key, value),
};
Object.defineProperty(window, "localStorage", {
configurable: true,
value: storage,
});
storage.setItem(AUTH_TOKEN_KEY, "browser-user-token");
});
afterEach(() => {
storage.clear();
vi.unstubAllGlobals();
});
describe("useModels", () => {
it("loads once for a user and retains a persisted model present in the catalog", async () => {
const fetchMock = vi.fn().mockResolvedValue(
Response.json({
models: [
{ id: "auto", name: "Aegida Auto", description: "Auto" },
{ id: "qwen", name: "Qwen", description: "Qwen" },
],
}),
);
vi.stubGlobal("fetch", fetchMock);
const { result, rerender } = renderHook(
({ userId }) => useModels(userId, "qwen"),
{ initialProps: { userId: "user-1" } },
);
await waitFor(() => expect(result.current.status).toBe("ready"));
expect(result.current.effectiveModelId).toBe("qwen");
rerender({ userId: "user-1" });
expect(fetchMock).toHaveBeenCalledOnce();
});
it("falls back to only auto when the catalog fails or the persisted model is absent", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
Response.json({
models: [{ id: "auto", name: "Aegida Auto", description: "Auto" }],
}),
)
.mockRejectedValueOnce(new Error("network-private-sentinel"));
vi.stubGlobal("fetch", fetchMock);
const first = renderHook(() => useModels("user-1", "removed-model"));
await waitFor(() => expect(first.result.current.status).toBe("ready"));
expect(first.result.current.models.map(({ id }) => id)).toEqual(["auto"]);
expect(first.result.current.effectiveModelId).toBe("auto");
first.unmount();
const second = renderHook(() => useModels("user-2", "qwen"));
await waitFor(() => expect(second.result.current.status).toBe("unavailable"));
expect(second.result.current.models.map(({ id }) => id)).toEqual(["auto"]);
expect(second.result.current.effectiveModelId).toBe("auto");
});
it("aborts a stale user request so it cannot overwrite the current catalog", async () => {
const pending: Array<{
signal: AbortSignal;
resolve(response: Response): void;
}> = [];
vi.stubGlobal(
"fetch",
vi.fn((_input: RequestInfo | URL, init?: RequestInit) =>
new Promise<Response>((resolve) => {
pending.push({ signal: init?.signal as AbortSignal, resolve });
}),
),
);
const { result, rerender } = renderHook(
({ userId, model }) => useModels(userId, model),
{ initialProps: { userId: "user-1", model: "old-model" } },
);
await waitFor(() => expect(pending).toHaveLength(1));
rerender({ userId: "user-2", model: "new-model" });
await waitFor(() => expect(pending).toHaveLength(2));
expect(pending[0].signal.aborted).toBe(true);
await act(async () => {
pending[1].resolve(
Response.json({
models: [
{ id: "auto", name: "Aegida Auto", description: "Auto" },
{ id: "new-model", name: "New", description: "New" },
],
}),
);
});
await waitFor(() => expect(result.current.effectiveModelId).toBe("new-model"));
await act(async () => {
pending[0].resolve(
Response.json({
models: [
{ id: "auto", name: "Aegida Auto", description: "Auto" },
{ id: "old-model", name: "Old", description: "Old" },
],
}),
);
});
expect(result.current.effectiveModelId).toBe("new-model");
});
});