Files
silo-server/web/src/hooks/queries/settingValuesMutations.test.tsx
T
QuickandGitHub b0418a4430 fix(web): stop library state request loop (#536)
* fix(web): stop library state request loop

* fix(web): harden library state persistence

* fix(web): reconcile rejected library state writes

* fix(web): make library state persistence lossless

* fix(web): serialize library state across remounts

* fix(web): pin profile identity across auth refresh

* fix(web): scope profile verification cleanup

* fix(web): isolate library state by account
2026-08-03 21:51:50 -04:00

194 lines
7.4 KiB
TypeScript

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, renderHook } from "@testing-library/react";
import type { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ApiClientError } from "@/api/client";
import { SETTING_KEYS } from "@/lib/settingsContract";
import { deviceKeys, settingsKeys } from "./keys";
import { useClearSettingValue, useSetSettingValue } from "./settingValues";
const apiMock = vi.hoisted(() => vi.fn());
vi.mock("@/api/client", async () => {
const actual = await vi.importActual<typeof import("@/api/client")>("@/api/client");
return { ...actual, api: apiMock };
});
function createHarness() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return { queryClient, wrapper };
}
describe("typed setting mutations", () => {
beforeEach(() => {
apiMock.mockReset();
});
it("does not invalidate effective settings after a definitive rejected write", async () => {
apiMock.mockRejectedValueOnce(new ApiClientError(429, "rate_limited", "rate limited"));
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useSetSettingValue(), { wrapper });
await act(async () => {
await expect(
result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
value: { version: 1, libraries: {} },
identity: { scope: "profile_device" },
}),
).rejects.toThrow("rate limited");
});
expect(invalidateQueries).not.toHaveBeenCalled();
});
it("invalidates effective settings and device summaries after a successful device write", async () => {
apiMock.mockResolvedValueOnce({});
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useSetSettingValue(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
value: { version: 1, libraries: {} },
identity: { scope: "profile_device" },
});
});
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: [...settingsKeys.all, "values"],
});
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: deviceKeys.all });
});
it("reconciles effective settings and device summaries after an ambiguous write failure", async () => {
apiMock.mockRejectedValueOnce(new TypeError("network connection lost"));
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useSetSettingValue(), { wrapper });
await act(async () => {
await expect(
result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
value: { version: 1, libraries: {} },
identity: { scope: "profile_device" },
}),
).rejects.toThrow("network connection lost");
});
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: [...settingsKeys.all, "values"],
});
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: deviceKeys.all });
});
it("reconciles effective settings and device summaries after a server write failure", async () => {
apiMock.mockRejectedValueOnce(new ApiClientError(503, "unavailable", "service unavailable"));
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useSetSettingValue(), { wrapper });
await act(async () => {
await expect(
result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
value: { version: 1, libraries: {} },
identity: { scope: "profile_device" },
}),
).rejects.toThrow("service unavailable");
});
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: [...settingsKeys.all, "values"],
});
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: deviceKeys.all });
});
it("does not invalidate effective settings after a definitive rejected clear", async () => {
apiMock.mockRejectedValueOnce(new ApiClientError(429, "rate_limited", "rate limited"));
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useClearSettingValue(), { wrapper });
await act(async () => {
await expect(
result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
identity: { scope: "profile_device" },
}),
).rejects.toThrow("rate limited");
});
expect(invalidateQueries).not.toHaveBeenCalled();
});
it("reconciles effective settings and device summaries after an already-cleared response", async () => {
apiMock.mockRejectedValueOnce(new ApiClientError(404, "not_found", "setting not found"));
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useClearSettingValue(), { wrapper });
await act(async () => {
await expect(
result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
identity: { scope: "profile_device" },
}),
).rejects.toThrow("setting not found");
});
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: [...settingsKeys.all, "values"],
});
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: deviceKeys.all });
});
it("invalidates effective settings and device summaries after a successful device clear", async () => {
apiMock.mockResolvedValueOnce({});
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useClearSettingValue(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
identity: { scope: "profile_device" },
});
});
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: [...settingsKeys.all, "values"],
});
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: deviceKeys.all });
});
it("reconciles effective settings and device summaries after an ambiguous clear failure", async () => {
apiMock.mockRejectedValueOnce(new TypeError("response connection lost"));
const { queryClient, wrapper } = createHarness();
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const { result } = renderHook(() => useClearSettingValue(), { wrapper });
await act(async () => {
await expect(
result.current.mutateAsync({
key: SETTING_KEYS.UI_LIBRARY_PAGE_STATE,
identity: { scope: "profile_device" },
}),
).rejects.toThrow("response connection lost");
});
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: [...settingsKeys.all, "values"],
});
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: deviceKeys.all });
});
});