import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { getFunctionName } from 'convex/server'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { api } from '@spoon/backend/convex/_generated/api.js'; import { SpoonAgentSettingsForm } from '../../src/components/spoons/spoon-agent-settings-form'; const { mockUseMutation, mockUseQuery } = vi.hoisted(() => ({ mockUseMutation: vi.fn(), mockUseQuery: vi.fn(), })); vi.mock('convex/react', () => ({ useMutation: mockUseMutation, useQuery: mockUseQuery, })); vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn(), }, })); const spoon = { _id: 'spoon-1', forkDefaultBranch: 'main', upstreamDefaultBranch: 'main', } as never; const configuredProfile = { _id: 'profile-1', name: 'OpenAI', provider: 'openai', enabled: true, configured: true, isDefault: true, supportedRuntimes: ['codex'], reasoningEffort: 'high', }; const modelCatalog = { profiles: [ { profileId: 'profile-1', configured: true, defaultModel: 'gpt-5', models: [{ id: 'gpt-5', label: 'GPT-5' }], }, ], }; const loadedSettings = { _id: 'settings-1' as never, enabled: true, runtime: 'codex' as const, defaultBaseBranch: 'develop', branchPrefix: 'custom/prefix', installCommand: 'bun install', agentModel: 'gpt-5', reasoningEffort: 'high' as const, envFilePath: '.env', aiProviderProfileId: 'profile-1' as never, }; const wireQueries = () => { const listMineName = getFunctionName(api.aiProviderProfiles.listMine); const catalogName = getFunctionName(api.aiProviderModels.listAvailableForUser); mockUseQuery.mockImplementation( (ref: Parameters[0]) => { const name = getFunctionName(ref); if (name === listMineName) return [configuredProfile]; if (name === catalogName) return modelCatalog; return undefined; }, ); }; afterEach(() => { cleanup(); vi.clearAllMocks(); }); describe('SpoonAgentSettingsForm re-sync on loaded record', () => { it('reflects the loaded settings after the record resolves late', async () => { wireQueries(); mockUseMutation.mockReturnValue(vi.fn()); // Parent gates on a different query, so this form mounts before the // agent settings query resolves (settings === undefined). const { rerender } = render( , ); expect(screen.getByLabelText(/branch prefix/i)).toHaveValue('spoon/agent'); // The settings query resolves with a saved record carrying non-defaults. rerender(); await waitFor(() => expect(screen.getByLabelText(/branch prefix/i)).toHaveValue( 'custom/prefix', ), ); expect(screen.getByLabelText(/default base branch/i)).toHaveValue( 'develop', ); expect(screen.getByLabelText(/install command/i)).toHaveValue( 'bun install', ); }); it('saves the loaded values, not the stale defaults, after a late resolve', async () => { wireQueries(); const update = vi.fn().mockResolvedValue('settings-1'); mockUseMutation.mockReturnValue(update); const { rerender } = render( , ); rerender(); // Wait for the late record to hydrate the form before saving. await waitFor(() => expect(screen.getByLabelText(/branch prefix/i)).toHaveValue( 'custom/prefix', ), ); fireEvent.click( screen.getByRole('button', { name: /save agent settings/i }), ); await waitFor(() => expect(update).toHaveBeenCalledTimes(1)); expect(update).toHaveBeenCalledWith( expect.objectContaining({ branchPrefix: 'custom/prefix', defaultBaseBranch: 'develop', installCommand: 'bun install', agentModel: 'gpt-5', }), ); }); });