Files
fusion/packages/dashboard/app/hooks/__tests__/useMemoryBackendStatus.test.ts
2026-04-25 18:22:37 -07:00

267 lines
9.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { act, renderHook, waitFor } from "@testing-library/react";
import { useMemoryBackendStatus } from "../useMemoryBackendStatus";
import * as api from "../../api";
describe("useMemoryBackendStatus", () => {
const mockFetchMemoryBackendStatus = vi.spyOn(api, "fetchMemoryBackendStatus");
const mockFileBackendStatus: api.MemoryBackendStatus = {
currentBackend: "file",
capabilities: {
readable: true,
writable: true,
supportsAtomicWrite: true,
hasConflictResolution: false,
persistent: true,
},
availableBackends: ["file", "readonly", "qmd"],
};
const mockReadonlyBackendStatus: api.MemoryBackendStatus = {
currentBackend: "readonly",
capabilities: {
readable: true,
writable: false,
supportsAtomicWrite: false,
hasConflictResolution: false,
persistent: false,
},
availableBackends: ["file", "readonly", "qmd"],
};
const mockQmdBackendStatus: api.MemoryBackendStatus = {
currentBackend: "qmd",
capabilities: {
readable: true,
writable: true,
supportsAtomicWrite: false,
hasConflictResolution: false,
persistent: true,
},
availableBackends: ["file", "readonly", "qmd"],
};
beforeEach(() => {
mockFetchMemoryBackendStatus.mockClear();
});
it("fetches status on initial mount", async () => {
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
expect(result.current.loading).toBe(true);
expect(result.current.status).toBeNull();
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.status).toEqual(mockFileBackendStatus);
expect(result.current.currentBackend).toBe("file");
expect(result.current.capabilities).toEqual(mockFileBackendStatus.capabilities);
expect(result.current.availableBackends).toEqual(["file", "readonly", "qmd"]);
expect(result.current.error).toBeNull();
expect(result.current.lastUpdated).toBeInstanceOf(Date);
});
it("does not fetch when enabled is false", () => {
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
const { result } = renderHook(() =>
useMemoryBackendStatus({ autoRefresh: false, enabled: false }),
);
expect(mockFetchMemoryBackendStatus).not.toHaveBeenCalled();
expect(result.current.loading).toBe(false);
expect(result.current.status).toBeNull();
});
it("fetches when enabled toggles from false to true", async () => {
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
const { result, rerender } = renderHook(
({ enabled }) => useMemoryBackendStatus({ autoRefresh: false, enabled }),
{ initialProps: { enabled: false } },
);
expect(mockFetchMemoryBackendStatus).not.toHaveBeenCalled();
expect(result.current.loading).toBe(false);
rerender({ enabled: true });
await waitFor(() => expect(result.current.loading).toBe(false));
expect(mockFetchMemoryBackendStatus).toHaveBeenCalledTimes(1);
expect(result.current.status).toEqual(mockFileBackendStatus);
});
it("handles fetch errors", async () => {
mockFetchMemoryBackendStatus.mockRejectedValue(new Error("Failed to connect"));
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe("Failed to connect");
expect(result.current.status).toBeNull();
});
it("provides correct capability flags for file backend", async () => {
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isReadable).toBe(true);
expect(result.current.isWritable).toBe(true);
expect(result.current.supportsAtomicWrite).toBe(true);
});
it("provides correct capability flags for readonly backend", async () => {
mockFetchMemoryBackendStatus.mockResolvedValue(mockReadonlyBackendStatus);
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isReadable).toBe(true);
expect(result.current.isWritable).toBe(false);
expect(result.current.supportsAtomicWrite).toBe(false);
});
it("provides correct capability flags for qmd backend", async () => {
mockFetchMemoryBackendStatus.mockResolvedValue(mockQmdBackendStatus);
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isReadable).toBe(true);
expect(result.current.isWritable).toBe(true);
expect(result.current.supportsAtomicWrite).toBe(false);
});
it("refreshes data manually", async () => {
mockFetchMemoryBackendStatus
.mockResolvedValueOnce(mockFileBackendStatus)
.mockResolvedValueOnce(mockQmdBackendStatus);
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.currentBackend).toBe("file");
// Manual refresh
await result.current.refresh();
await waitFor(() => expect(result.current.currentBackend).toBe("qmd"));
});
it("treats backend status as unknown while a refresh is in flight", async () => {
let resolveRefresh: ((value: api.MemoryBackendStatus) => void) | null = null;
mockFetchMemoryBackendStatus
.mockResolvedValueOnce(mockReadonlyBackendStatus)
.mockImplementationOnce(
() => new Promise<api.MemoryBackendStatus>((resolve) => {
resolveRefresh = resolve;
}),
);
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isWritable).toBe(false);
expect(result.current.status).toEqual(mockReadonlyBackendStatus);
act(() => {
void result.current.refresh();
});
await waitFor(() => expect(result.current.loading).toBe(true));
expect(result.current.status).toBeNull();
expect(result.current.capabilities).toBeNull();
expect(result.current.isWritable).toBe(false);
act(() => {
resolveRefresh?.(mockFileBackendStatus);
});
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.status).toEqual(mockFileBackendStatus);
expect(result.current.isWritable).toBe(true);
});
it("clears error on successful refresh after error", async () => {
mockFetchMemoryBackendStatus
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValueOnce(mockFileBackendStatus);
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe("Network error");
// Manual refresh
await result.current.refresh();
await waitFor(() => expect(result.current.error).toBeNull());
expect(result.current.status).toEqual(mockFileBackendStatus);
});
it("passes projectId to API", async () => {
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
const { result } = renderHook(() =>
useMemoryBackendStatus({ projectId: "proj_abc", autoRefresh: false }),
);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(mockFetchMemoryBackendStatus).toHaveBeenCalledWith("proj_abc");
});
it("returns expected default values before first fetch", () => {
mockFetchMemoryBackendStatus.mockImplementation(() => new Promise(() => {})); // Never resolves
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
expect(result.current.status).toBeNull();
expect(result.current.currentBackend).toBeNull();
expect(result.current.capabilities).toBeNull();
expect(result.current.availableBackends).toEqual([]);
expect(result.current.isReadable).toBe(false);
expect(result.current.isWritable).toBe(false);
expect(result.current.supportsAtomicWrite).toBe(false);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeNull();
expect(result.current.lastUpdated).toBeNull();
expect(typeof result.current.refresh).toBe("function");
});
it("exports the correct interface", () => {
expect(typeof useMemoryBackendStatus).toBe("function");
});
it("handles null error messages", async () => {
mockFetchMemoryBackendStatus.mockRejectedValue(new Error());
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
// Error with no message results in empty string
expect(result.current.error).toBe("");
});
it("handles non-Error rejections", async () => {
mockFetchMemoryBackendStatus.mockRejectedValue("string error");
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe("Failed to fetch memory backend status");
});
});