fix(ui): re-sync settings/thread forms on loaded record
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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<typeof getFunctionName>[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(
|
||||
<SpoonAgentSettingsForm spoon={spoon} settings={undefined} />,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(/branch prefix/i)).toHaveValue('spoon/agent');
|
||||
|
||||
// The settings query resolves with a saved record carrying non-defaults.
|
||||
rerender(<SpoonAgentSettingsForm spoon={spoon} settings={loadedSettings} />);
|
||||
|
||||
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(
|
||||
<SpoonAgentSettingsForm spoon={spoon} settings={undefined} />,
|
||||
);
|
||||
rerender(<SpoonAgentSettingsForm spoon={spoon} settings={loadedSettings} />);
|
||||
|
||||
// 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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user