fix(FN-2704): gate auth onboarding behind setup wizard

- Prevent useAuthOnboarding from opening model onboarding while the setup wizard is active
- Add app-level wiring so onboarding flow respects setup wizard visibility state
- Add regression tests covering setup wizard gating and non-stacking onboarding behavior
This commit is contained in:
Fusion
2026-04-27 09:56:05 -07:00
committed by gsxdsm
parent 637f4350c9
commit bd59b1ff99
3 changed files with 78 additions and 1 deletions

View File

@@ -393,6 +393,7 @@ function AppInner() {
// Auth and onboarding bootstrap logic extracted to a dedicated hook. // Auth and onboarding bootstrap logic extracted to a dedicated hook.
useAuthOnboarding({ useAuthOnboarding({
projectId: currentProject?.id, projectId: currentProject?.id,
setupWizardOpen: modalManager.setupWizardOpen,
openModelOnboarding: modalManager.openModelOnboarding, openModelOnboarding: modalManager.openModelOnboarding,
openSettings: modalManager.openSettings, openSettings: modalManager.openSettings,
}); });

View File

@@ -49,6 +49,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -76,6 +77,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -101,6 +103,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -127,6 +130,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -152,6 +156,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -173,6 +178,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -187,6 +193,66 @@ describe("useAuthOnboarding", () => {
expect(mockFetchGlobalSettings).not.toHaveBeenCalled(); expect(mockFetchGlobalSettings).not.toHaveBeenCalled();
}); });
it("does not auto-trigger model onboarding when setupWizardOpen is true", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [{ id: "openai", name: "OpenAI", authenticated: false }],
});
mockFetchGlobalSettings.mockResolvedValue({
modelOnboardingComplete: false,
defaultProvider: undefined,
defaultModelId: undefined,
} as never);
renderHook(() =>
useAuthOnboarding({
projectId: "proj_123",
setupWizardOpen: true,
openModelOnboarding,
openSettings,
}),
);
await waitFor(() => {
expect(openModelOnboarding).not.toHaveBeenCalled();
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
});
});
it("auto-triggers model onboarding when setupWizardOpen transitions from true to false", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [{ id: "openai", name: "OpenAI", authenticated: false }],
});
mockFetchGlobalSettings.mockResolvedValue({
modelOnboardingComplete: false,
defaultProvider: undefined,
defaultModelId: undefined,
} as never);
const { rerender } = renderHook(
({ setupWizardOpen }: { setupWizardOpen: boolean }) =>
useAuthOnboarding({
projectId: "proj_123",
setupWizardOpen,
openModelOnboarding,
openSettings,
}),
{
initialProps: { setupWizardOpen: true },
},
);
await waitFor(() => {
expect(openModelOnboarding).not.toHaveBeenCalled();
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
});
rerender({ setupWizardOpen: false });
await waitFor(() => {
expect(openModelOnboarding).toHaveBeenCalledTimes(1);
});
});
// --- One-shot guard --- // --- One-shot guard ---
it("does not re-trigger onboarding when projectId changes after initial bootstrap", async () => { it("does not re-trigger onboarding when projectId changes after initial bootstrap", async () => {
@@ -204,6 +270,7 @@ describe("useAuthOnboarding", () => {
({ projectId }: { projectId: string }) => ({ projectId }: { projectId: string }) =>
useAuthOnboarding({ useAuthOnboarding({
projectId, projectId,
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -238,6 +305,7 @@ describe("useAuthOnboarding", () => {
({ open }: { open: () => void }) => ({ open }: { open: () => void }) =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding: open, openModelOnboarding: open,
openSettings, openSettings,
}), }),
@@ -273,6 +341,7 @@ describe("useAuthOnboarding", () => {
({ open }: { open: (section?: string) => void }) => ({ open }: { open: (section?: string) => void }) =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings: open, openSettings: open,
}), }),
@@ -311,6 +380,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),
@@ -341,6 +411,7 @@ describe("useAuthOnboarding", () => {
renderHook(() => renderHook(() =>
useAuthOnboarding({ useAuthOnboarding({
projectId: "proj_123", projectId: "proj_123",
setupWizardOpen: false,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}), }),

View File

@@ -6,6 +6,7 @@ import type { SectionId } from "../components/SettingsModal";
export interface UseAuthOnboardingOptions { export interface UseAuthOnboardingOptions {
projectId?: string; projectId?: string;
setupWizardOpen: boolean;
openModelOnboarding: () => void; openModelOnboarding: () => void;
openSettings: (section?: SectionId) => void; openSettings: (section?: SectionId) => void;
} }
@@ -24,6 +25,7 @@ export interface UseAuthOnboardingOptions {
*/ */
export function useAuthOnboarding({ export function useAuthOnboarding({
projectId, projectId,
setupWizardOpen,
openModelOnboarding, openModelOnboarding,
openSettings, openSettings,
}: UseAuthOnboardingOptions): void { }: UseAuthOnboardingOptions): void {
@@ -32,6 +34,9 @@ export function useAuthOnboarding({
const hasTriggeredRef = useRef(false); const hasTriggeredRef = useRef(false);
useEffect(() => { useEffect(() => {
// Defer auto-triggering while setup wizard is open.
// Important: this must run before consuming the one-shot flag.
if (setupWizardOpen) return;
// Skip if we've already triggered (one-shot guard) // Skip if we've already triggered (one-shot guard)
if (hasTriggeredRef.current) return; if (hasTriggeredRef.current) return;
// Mark as triggered immediately to prevent any race condition on re-runs // Mark as triggered immediately to prevent any race condition on re-runs
@@ -82,5 +87,5 @@ export function useAuthOnboarding({
// Fail silently - non-blocking behavior preserves dashboard usability. // Fail silently - non-blocking behavior preserves dashboard usability.
// Onboarding can be manually triggered later via Settings if needed. // Onboarding can be manually triggered later via Settings if needed.
}); });
}, [projectId, openModelOnboarding, openSettings]); }, [projectId, setupWizardOpen, openModelOnboarding, openSettings]);
} }