fix(dashboard): hand off cleanly from setup wizard to model onboarding
On a fresh install useAuthOnboarding's effect ran at mount before the setup wizard's 500ms auto-open timer fired. The one-shot ref locked, and the resolved fetch could either stack model onboarding on top of the wizard or never re-trigger after the wizard closed. - Gate the trigger on projectId being set so the wizard owns the bootstrap phase; the auth check only fires once a project exists. - Re-check setupWizardOpen via a ref when the auth fetch resolves to avoid stacking onboarding on top of a wizard opened mid-fetch. - Release the one-shot in that suppressed branch so the effect retries when the wizard closes. Adds two regression tests: fresh-install handoff and mid-fetch wizard suppression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -253,6 +253,86 @@ describe("useAuthOnboarding", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not auto-trigger before a projectId exists (fresh install pre-wizard race)", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValue({
|
||||
providers: [{ id: "openai", name: "OpenAI", authenticated: false }],
|
||||
});
|
||||
mockFetchGlobalSettings.mockResolvedValue({
|
||||
modelOnboardingComplete: false,
|
||||
defaultProvider: undefined,
|
||||
defaultModelId: undefined,
|
||||
} as never);
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ projectId, setupWizardOpen }: { projectId: string | undefined; setupWizardOpen: boolean }) =>
|
||||
useAuthOnboarding({
|
||||
projectId,
|
||||
setupWizardOpen,
|
||||
openModelOnboarding,
|
||||
openSettings,
|
||||
}),
|
||||
{
|
||||
initialProps: { projectId: undefined as string | undefined, setupWizardOpen: false },
|
||||
},
|
||||
);
|
||||
|
||||
// No project yet — the setup wizard owns this phase. Don't fetch or open.
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
|
||||
expect(openModelOnboarding).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Setup wizard opens, user fills it out…
|
||||
rerender({ projectId: undefined, setupWizardOpen: true });
|
||||
// …completes it: project registered, wizard closes.
|
||||
rerender({ projectId: "proj_new", setupWizardOpen: false });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(openModelOnboarding).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not stack model onboarding on top of a setup wizard that opens mid-fetch", async () => {
|
||||
let resolveAuth: (value: { providers: Array<{ id: string; name: string; authenticated: boolean }> }) => void = () => {};
|
||||
mockFetchAuthStatus.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveAuth = resolve;
|
||||
}) as never,
|
||||
);
|
||||
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: false } },
|
||||
);
|
||||
|
||||
// Wizard opens while fetch is still pending.
|
||||
rerender({ setupWizardOpen: true });
|
||||
resolveAuth({ providers: [{ id: "openai", name: "OpenAI", authenticated: false }] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAuthStatus).toHaveBeenCalled();
|
||||
});
|
||||
// Onboarding must not open while wizard is up.
|
||||
expect(openModelOnboarding).not.toHaveBeenCalled();
|
||||
|
||||
// After wizard closes, onboarding takes over.
|
||||
rerender({ setupWizardOpen: false });
|
||||
await waitFor(() => {
|
||||
expect(openModelOnboarding).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// --- One-shot guard ---
|
||||
|
||||
it("does not re-trigger onboarding when projectId changes after initial bootstrap", async () => {
|
||||
|
||||
@@ -32,11 +32,22 @@ export function useAuthOnboarding({
|
||||
// One-shot guard: prevents the auto-trigger logic from running more than once
|
||||
// per hook instance, even if the effect re-runs due to dependency changes.
|
||||
const hasTriggeredRef = useRef(false);
|
||||
// Track latest setupWizardOpen so the resolved fetch promise can re-check
|
||||
// it without becoming a stale-closure read.
|
||||
const setupWizardOpenRef = useRef(setupWizardOpen);
|
||||
setupWizardOpenRef.current = setupWizardOpen;
|
||||
|
||||
useEffect(() => {
|
||||
// Defer auto-triggering while setup wizard is open.
|
||||
// Important: this must run before consuming the one-shot flag.
|
||||
if (setupWizardOpen) return;
|
||||
// Hold off until the user has a project. On a fresh install the setup
|
||||
// wizard opens ~500ms after mount, so without this gate the effect would
|
||||
// race ahead, lock the one-shot flag, and (a) potentially stack both
|
||||
// modals, or (b) never re-trigger model onboarding once the wizard
|
||||
// closes. With a project in scope, either the wizard already finished
|
||||
// or it was never going to open.
|
||||
if (!projectId) return;
|
||||
// Skip if we've already triggered (one-shot guard)
|
||||
if (hasTriggeredRef.current) return;
|
||||
// Mark as triggered immediately to prevent any race condition on re-runs
|
||||
@@ -74,7 +85,15 @@ export function useAuthOnboarding({
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Execute after the promise chain resolves
|
||||
// Execute after the promise chain resolves. Re-check the wizard:
|
||||
// the user (or auto-open logic) may have opened it while the auth
|
||||
// fetch was in flight, and we don't want to stack modals.
|
||||
if (setupWizardOpenRef.current) {
|
||||
// Release the one-shot so the effect can retry once the wizard
|
||||
// closes (the effect re-runs on the setupWizardOpen dep flip).
|
||||
hasTriggeredRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (shouldOpenOnboarding) {
|
||||
trackOnboardingEvent("onboarding:auto-triggered", { trigger: "first-run" });
|
||||
openModelOnboarding();
|
||||
|
||||
Reference in New Issue
Block a user