,
+}));
+
+const mockUseMutation = vi.mocked(useMutation);
+const mockUseQueryClient = vi.mocked(useQueryClient);
+
+import { StepConfigureInference } from './StepConfigureInference';
+
+describe('StepConfigureInference', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUseMutation.mockReturnValue({
+ mutate: mutateMock,
+ isPending: false,
+ } as unknown as ReturnType);
+ mockUseQueryClient.mockReturnValue({
+ invalidateQueries: vi.fn(),
+ } as unknown as ReturnType);
+ });
+
+ it('renders the trial and custom choices with the requested copy', () => {
+ render(
+ ,
+ );
+
+ expect(
+ screen.getByRole('heading', { name: 'Configure inference' }),
+ ).toBeInTheDocument();
+ expect(screen.getByText(/Roomote needs a model provider/).textContent).toBe(
+ 'Roomote needs a model provider for, you know, AI stuff.If you want, we can give you a few credits to try Roomote out or you can configure your provider directly.',
+ );
+ expect(
+ screen.getByRole('button', {
+ name: 'Use free Roomote trial inference',
+ }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Configure your provider' }),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('gift')).toBeInTheDocument();
+ expect(screen.getByTestId('plug')).toBeInTheDocument();
+ expect(
+ screen.getByText('Roomote trial inference goes through OpenRouter.'),
+ ).toBeInTheDocument();
+ });
+
+ it('starts trial inference and advances after the setup mutation succeeds', async () => {
+ const onUseTrial = vi.fn();
+ render(
+ ,
+ );
+
+ fireEvent.click(
+ screen.getByRole('button', {
+ name: 'Use free Roomote trial inference',
+ }),
+ );
+
+ expect(mutateMock).toHaveBeenCalledOnce();
+
+ const options = mockUseMutation.mock.calls[0]?.[0] as
+ | { onSuccess?: () => Promise | void }
+ | undefined;
+ await options?.onSuccess?.();
+ expect(onUseTrial).toHaveBeenCalledOnce();
+ });
+
+ it('opens custom provider configuration without mutating trial state', () => {
+ const onConfigureProvider = vi.fn();
+ render(
+ ,
+ );
+
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Configure your provider' }),
+ );
+
+ expect(onConfigureProvider).toHaveBeenCalledOnce();
+ expect(mutateMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/web/src/app/(onboarding)/setup/StepConfigureInference.tsx b/apps/web/src/app/(onboarding)/setup/StepConfigureInference.tsx
new file mode 100644
index 000000000..00c185c6f
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/setup/StepConfigureInference.tsx
@@ -0,0 +1,93 @@
+'use client';
+
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Gift, Plug } from 'lucide-react';
+import { toast } from 'sonner';
+
+import { useTRPC } from '@/trpc/client';
+import { ArrowRight, Button, Spinner } from '@/components/system';
+import { cn } from '@/lib/utils';
+
+import { StepTitle } from './StepTitle';
+import { getSetupStepDefinition } from './types';
+
+const INFERENCE_STEP = getSetupStepDefinition('inference');
+
+export function StepConfigureInference({
+ onUseTrial,
+ onConfigureProvider,
+}: {
+ onUseTrial: () => void;
+ onConfigureProvider: () => void;
+}) {
+ const trpc = useTRPC();
+ const queryClient = useQueryClient();
+ const chooseTrialInference = useMutation(
+ trpc.setupNew.chooseTrialInference.mutationOptions({
+ onSuccess: async () => {
+ await queryClient.invalidateQueries({
+ queryKey: trpc.setupNew.status.queryKey(),
+ });
+ onUseTrial();
+ },
+ onError: (error) => toast.error(error.message),
+ }),
+ );
+ const choiceButtonClassName = cn(
+ 'group flex w-full items-center gap-3 py-5',
+ 'hover:text-accent-foreground hover:bg-foreground',
+ );
+
+ return (
+
+
+
+
+ Roomote needs a model provider for, you know, AI stuff.
+
+ If you want, we can give you a few credits to try Roomote out or you
+ can configure your provider directly.
+
+
+
+
+
+
+
+
+ Roomote trial inference goes through OpenRouter.
+