Files
fusion/packages/dashboard/app/hooks/__tests__/useSetupReadiness.test.ts
Fusion e30bd2e24b feat(FN-1941): surface setup readiness warnings in task entry flows
- Add useSetupReadiness hook to evaluate setup state and expose actionable warning metadata
- Add reusable SetupWarningBanner component with compact and full warning presentation modes
- Render setup warnings in NewTaskModal and QuickEntryBox so task creation surfaces missing configuration early
- Add targeted tests for the hook and banner plus integration coverage updates for modal and quick-entry behavior
2026-04-17 04:46:28 -07:00

165 lines
5.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, waitFor, act } from "@testing-library/react";
import { useSetupReadiness, __test_clearCache } from "../useSetupReadiness";
import * as api from "../../api";
import type { AuthProvider } from "../../api";
vi.mock("../../api", () => ({
fetchAuthStatus: vi.fn(),
}));
const mockFetchAuthStatus = vi.mocked(api.fetchAuthStatus);
function makeProvider(
id: string,
authenticated: boolean,
name = id,
type: AuthProvider["type"] = "oauth",
): AuthProvider {
return {
id,
name,
authenticated,
type,
};
}
function deferred<T>(): {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (reason?: unknown) => void;
} {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
describe("useSetupReadiness", () => {
beforeEach(() => {
vi.clearAllMocks();
__test_clearCache();
});
it("returns hasAiProvider=true when at least one non-GitHub provider is authenticated", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("anthropic", true), makeProvider("github", false)],
});
const { result } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.hasAiProvider).toBe(true);
});
it("returns hasAiProvider=false when no non-GitHub providers are authenticated", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("anthropic", false), makeProvider("github", true)],
});
const { result } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.hasAiProvider).toBe(false);
});
it("returns hasGithub=true when GitHub provider is authenticated", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("anthropic", true), makeProvider("github", true)],
});
const { result } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.hasGithub).toBe(true);
});
it("returns hasGithub=false when GitHub is missing or not authenticated", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("anthropic", true)],
});
const { result: missingGithub } = renderHook(() => useSetupReadiness("project-a"));
await waitFor(() => expect(missingGithub.current.loading).toBe(false));
expect(missingGithub.current.hasGithub).toBe(false);
__test_clearCache();
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("github", false)],
});
const { result: unauthenticatedGithub } = renderHook(() => useSetupReadiness("project-b"));
await waitFor(() => expect(unauthenticatedGithub.current.loading).toBe(false));
expect(unauthenticatedGithub.current.hasGithub).toBe(false);
});
it("returns hasWarnings=true when AI provider is missing", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("github", true)],
});
const { result } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.hasWarnings).toBe(true);
});
it("returns hasWarnings=true when GitHub is missing", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("anthropic", true), makeProvider("github", false)],
});
const { result } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.hasWarnings).toBe(true);
});
it("returns hasWarnings=false when both AI provider and GitHub are connected", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [makeProvider("anthropic", true), makeProvider("github", true)],
});
const { result } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.hasWarnings).toBe(false);
});
it("returns loading=true during initial fetch and false after completion", async () => {
const pending = deferred<{ providers: AuthProvider[] }>();
mockFetchAuthStatus.mockReturnValueOnce(pending.promise);
const { result } = renderHook(() => useSetupReadiness());
expect(result.current.loading).toBe(true);
await act(async () => {
pending.resolve({
providers: [makeProvider("anthropic", true), makeProvider("github", true)],
});
});
await waitFor(() => expect(result.current.loading).toBe(false));
});
it("cache prevents duplicate fetches across multiple consumers", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [makeProvider("anthropic", true), makeProvider("github", true)],
});
const { result: first } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(first.current.loading).toBe(false));
const { result: second } = renderHook(() => useSetupReadiness());
await waitFor(() => expect(second.current.loading).toBe(false));
expect(mockFetchAuthStatus).toHaveBeenCalledTimes(1);
});
});