diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.test.tsx b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.test.tsx index 8e5912c7d4942..b8b86acf6a0f0 100644 --- a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.test.tsx +++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_flyout_wrapper.test.tsx @@ -15,14 +15,19 @@ import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { InferenceFlyoutWrapper } from './inference_flyout_wrapper'; import { QueryClient, QueryClientProvider } from '@kbn/react-query'; import { mockProviders } from '../utils/mock_providers'; +import type { InferenceProvider } from '../types/types'; const mockMutationFn = jest.fn(); const httpMock = httpServiceMock.createStartContract(); const notificationsMock = notificationServiceMock.createStartContract(); +// Create a stable cloned copy for each test to prevent mutations from affecting other tests +// Note: Variable must be prefixed with 'mock' to be allowed in jest.mock() +let mockClonedProviders: InferenceProvider[]; + jest.mock('../hooks/use_providers', () => ({ useProviders: jest.fn(() => ({ - data: mockProviders, + data: mockClonedProviders, })), })); @@ -63,6 +68,8 @@ describe('InferenceFlyout', () => { }; beforeEach(async () => { jest.clearAllMocks(); + // Reset cloned providers before each test to prevent mutation pollution + mockClonedProviders = JSON.parse(JSON.stringify(mockProviders)); }); it('renders', () => { @@ -198,4 +205,86 @@ describe('InferenceFlyout', () => { renderComponent({ isEdit: true, inferenceEndpoint: mockEndpoint }); expect(screen.getByTestId('num_allocations-number')).toBeEnabled(); }); + + // Note: UI visibility tests for serverless adaptive allocations are in inference_service_form_fields.test.tsx + // This file focuses on integration tests: form submission, deserialization, and cross-provider behavior + describe('Serverless adaptive allocations', () => { + it('does not affect other providers like Hugging Face', async () => { + renderComponent({ enforceAdaptiveAllocations: true }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Hugging Face')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Hugging Face'); + // Hugging Face fields should be visible as normal + expect(screen.getByTestId('api_key-password')).toBeInTheDocument(); + expect(screen.getByTestId('url-input')).toBeInTheDocument(); + // max_number_of_allocations should not be visible for non-elasticsearch providers + expect(screen.queryByTestId('max_number_of_allocations-number')).not.toBeInTheDocument(); + }); + + it('submits form with adaptive_allocations config when max_number_of_allocations is set', async () => { + renderComponent({ enforceAdaptiveAllocations: true }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + // Set max allocations + const maxAllocationsInput = screen.getByTestId('max_number_of_allocations-number'); + await userEvent.clear(maxAllocationsInput); + await userEvent.type(maxAllocationsInput, '10'); + + await userEvent.click(screen.getByTestId('inference-endpoint-submit-button')); + + expect(mockMutationFn).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + provider: 'elasticsearch', + providerConfig: expect.objectContaining({ + adaptive_allocations: expect.objectContaining({ + enabled: true, + min_number_of_allocations: 0, + max_number_of_allocations: 10, + }), + num_threads: 1, + }), + }), + }), + false + ); + }); + + describe('edit mode with adaptive allocations', () => { + it('deserializes adaptive_allocations.max_number_of_allocations for display in serverless', () => { + const mockEndpoint = { + config: { + inferenceId: 'test-id', + provider: 'elasticsearch', + taskType: 'text_embedding', + providerConfig: { + model_id: '.elser_model_2', + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 0, + max_number_of_allocations: 5, + }, + }, + }, + secrets: { + providerSecrets: {}, + }, + }; + + renderComponent({ + isEdit: true, + enforceAdaptiveAllocations: true, + inferenceEndpoint: mockEndpoint, + }); + + // max_number_of_allocations should be shown with the deserialized value + expect(screen.getByTestId('max_number_of_allocations-number')).toBeInTheDocument(); + expect(screen.getByTestId('max_number_of_allocations-number')).toHaveValue(5); + }); + }); + }); }); diff --git a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx index 0b6cb737a2ae0..5db3c41b14ddc 100644 --- a/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx +++ b/x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/inference_service_form_fields.test.tsx @@ -19,9 +19,13 @@ import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { mockProviders } from '../utils/mock_providers'; import type { InferenceProvider } from '../types/types'; +// Create a stable cloned copy for each test suite to prevent mutations from affecting other tests +// Note: Variable must be prefixed with 'mock' to be allowed in jest.mock() +let mockClonedProviders: InferenceProvider[]; + jest.mock('../hooks/use_providers', () => ({ useProviders: jest.fn(() => ({ - data: mockProviders, + data: mockClonedProviders, })), })); @@ -38,34 +42,44 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => { ); }; +interface RenderFormOptions { + enforceAdaptiveAllocations?: boolean; +} + +const renderForm = (options: RenderFormOptions = {}) => { + const { enforceAdaptiveAllocations } = options; + + return render( + + + + ); +}; + describe('Inference Services', () => { + // Reset cloned providers before each test to prevent mutation pollution + beforeEach(() => { + mockClonedProviders = JSON.parse(JSON.stringify(mockProviders)); + }); it('renders', () => { - render( - - - - ); + renderForm(); expect(screen.getByTestId('provider-select')).toBeInTheDocument(); }); it('renders Selectable', async () => { - render( - - - - ); + renderForm(); await userEvent.click(screen.getByTestId('provider-select')); expect(screen.getByTestId('euiSelectableList')).toBeInTheDocument(); }); it('renders Elastic at top', async () => { - render( - - - - ); + renderForm(); await userEvent.click(screen.getByTestId('provider-select')); const listItems = screen.getAllByTestId('provider'); @@ -73,11 +87,7 @@ describe('Inference Services', () => { }); it('renders selected provider fields - hugging_face', async () => { - render( - - - - ); + renderForm(); await userEvent.click(screen.getByTestId('provider-select')); await userEvent.click(screen.getByText('Hugging Face')); @@ -93,11 +103,7 @@ describe('Inference Services', () => { }); it('re-renders fields when selected to anthropic from hugging_face', async () => { - render( - - - - ); + renderForm(); await userEvent.click(screen.getByTestId('provider-select')); await userEvent.click(screen.getByText('Hugging Face')); @@ -130,4 +136,122 @@ describe('Inference Services', () => { expect(isProviderForSolutions('security', provider)).toBe(false); }); }); + + describe('Serverless adaptive allocations', () => { + describe('when enforceAdaptiveAllocations is true (serverless)', () => { + it('shows max_number_of_allocations field for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: true }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // max_number_of_allocations should be visible in serverless + expect(screen.getByTestId('max_number_of_allocations-number')).toBeInTheDocument(); + }); + + it('hides num_allocations field for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: true }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // num_allocations should be hidden in serverless + expect(screen.queryByTestId('num_allocations-number')).not.toBeInTheDocument(); + }); + + it('hides num_threads field for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: true }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // num_threads should be hidden in serverless + expect(screen.queryByTestId('num_threads-number')).not.toBeInTheDocument(); + }); + + it('shows adaptive resources title for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: true }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // Adaptive resources title should be shown in serverless + expect(screen.getByTestId('maxNumberOfAllocationsDetailsLabel')).toBeInTheDocument(); + }); + }); + + describe('when enforceAdaptiveAllocations is false (non-serverless)', () => { + it('shows num_allocations field for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: false }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // num_allocations should be visible in non-serverless + expect(screen.getByTestId('num_allocations-number')).toBeInTheDocument(); + }); + + it('shows num_threads field for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: false }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // num_threads should be visible in non-serverless + expect(screen.getByTestId('num_threads-number')).toBeInTheDocument(); + }); + + it('does not show max_number_of_allocations field for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: false }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // max_number_of_allocations should NOT be visible in non-serverless + expect(screen.queryByTestId('max_number_of_allocations-number')).not.toBeInTheDocument(); + }); + + it('does not show adaptive resources title for Elasticsearch provider', async () => { + renderForm({ enforceAdaptiveAllocations: false }); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // Adaptive resources title should NOT be shown in non-serverless + expect(screen.queryByTestId('maxNumberOfAllocationsDetailsLabel')).not.toBeInTheDocument(); + }); + }); + + describe('when enforceAdaptiveAllocations is not provided (defaults to false)', () => { + it('shows num_allocations field for Elasticsearch provider', async () => { + renderForm(); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // num_allocations should be visible when enforceAdaptiveAllocations is not set + expect(screen.getByTestId('num_allocations-number')).toBeInTheDocument(); + }); + + it('does not show max_number_of_allocations field for Elasticsearch provider', async () => { + renderForm(); + + await userEvent.click(screen.getByTestId('provider-select')); + await userEvent.click(screen.getByText('Elasticsearch')); + + expect(screen.getByTestId('provider-select')).toHaveValue('Elasticsearch'); + // max_number_of_allocations should NOT be visible when enforceAdaptiveAllocations is not set + expect(screen.queryByTestId('max_number_of_allocations-number')).not.toBeInTheDocument(); + }); + }); + }); });