Files
semantic-kernel/python/tests/conftest.py
e0bd528cd9 Python: Default Dapr module allowlist to semantic_kernel prefix (#13596)
### Motivation and Context

Follow-up to #13499. The previous PR added the `allowed_module_prefixes`
parameter but defaulted it to `None`, which meant the module restriction
was only active if developers discovered and configured it.
Secure-by-default is the right posture here — restrict first, let
developers widen as needed.

- Change `allowed_module_prefixes` default from `None` to
`("semantic_kernel.",)` across Dapr runtime step loading
- Non-SK step classes now require developers to explicitly add their
module prefix (e.g. `("semantic_kernel.", "myapp.steps.")`)
- Developers can pass `None` to opt out entirely, but the secure default
is now enforced
- The Dapr runtime code is experimental, so this is a non-breaking
change per our stability guarantees

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [X] The code builds clean without any errors or warnings
- [X] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [X] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄

---------

Co-authored-by: MAF Dashboard Bot <[email protected]>
Co-authored-by: Copilot <[email protected]>
2026-03-24 11:34:25 +09:00

460 lines
14 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated, Any
from unittest.mock import MagicMock
from uuid import uuid4
from pydantic import BaseModel
from pytest import fixture
from semantic_kernel.agents import Agent, DeclarativeSpecMixin, register_agent_type
from semantic_kernel.data.vector import VectorStoreCollectionDefinition, VectorStoreField, vectorstoremodel
if TYPE_CHECKING:
from semantic_kernel import Kernel
from semantic_kernel.contents import ChatHistory
from semantic_kernel.filters import FunctionInvocationContext
from semantic_kernel.functions import KernelFunction
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
def pytest_configure(config):
logging.basicConfig(level=logging.ERROR)
logging.getLogger("tests.utils").setLevel(logging.INFO)
logging.getLogger("openai").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("semantic_kernel").setLevel(logging.INFO)
# region: Kernel fixtures
@fixture(scope="function")
def kernel() -> "Kernel":
from semantic_kernel import Kernel
return Kernel()
@fixture(scope="session")
def service() -> "AIServiceClientBase":
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
return AIServiceClientBase(service_id="service", ai_model_id="ai_model_id")
@fixture(scope="session")
def default_service() -> "AIServiceClientBase":
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
return AIServiceClientBase(service_id="default", ai_model_id="ai_model_id")
@fixture(scope="function")
def kernel_with_service(kernel: "Kernel", service: "AIServiceClientBase") -> "Kernel":
kernel.add_service(service)
return kernel
@fixture(scope="function")
def kernel_with_default_service(kernel: "Kernel", default_service: "AIServiceClientBase") -> "Kernel":
kernel.add_service(default_service)
return kernel
@fixture(scope="session")
def not_decorated_native_function() -> Callable:
def not_decorated_native_function(arg1: str) -> str:
return "test"
return not_decorated_native_function
@fixture(scope="session")
def decorated_native_function() -> Callable:
from semantic_kernel.functions.kernel_function_decorator import kernel_function
@kernel_function(name="getLightStatus")
def decorated_native_function(arg1: str) -> str:
return "test"
return decorated_native_function
@fixture(scope="session")
def custom_plugin_class():
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class CustomPlugin:
@kernel_function(name="getLightStatus")
def decorated_native_function(self) -> str:
return "test"
return CustomPlugin
@fixture(scope="session")
def experimental_plugin_class():
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class ExperimentalPlugin:
@kernel_function(name="getLightStatus")
def decorated_native_function(self) -> str:
return "test"
return ExperimentalPlugin
@fixture(scope="session")
def auto_function_invocation_filter() -> Callable:
"""A filter that will be called for each function call in the response."""
from semantic_kernel.filters import AutoFunctionInvocationContext
async def auto_function_invocation_filter(context: AutoFunctionInvocationContext, next):
await next(context)
context.terminate = True
return auto_function_invocation_filter
@fixture(scope="session")
def create_mock_function() -> Callable:
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.functions.function_result import FunctionResult
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
async def stream_func(*args, **kwargs):
yield [StreamingTextContent(choice_index=0, text="test", metadata={})]
def create_mock_function(name: str, value: str = "test") -> "KernelFunction":
kernel_function_metadata = KernelFunctionMetadata(
name=name,
plugin_name="TestPlugin",
description="test description",
parameters=[],
is_prompt=True,
is_asynchronous=True,
)
class CustomKernelFunction(KernelFunction):
call_count: int = 0
async def _invoke_internal_stream(
self,
context: "FunctionInvocationContext",
) -> None:
self.call_count += 1
context.result = FunctionResult(
function=kernel_function_metadata,
value=stream_func(),
)
async def _invoke_internal(self, context: "FunctionInvocationContext"):
self.call_count += 1
context.result = FunctionResult(function=kernel_function_metadata, value=value, metadata={})
return CustomKernelFunction(metadata=kernel_function_metadata)
return create_mock_function
@fixture(scope="function")
def get_tool_call_mock():
from semantic_kernel.contents.function_call_content import FunctionCallContent
tool_call_mock = MagicMock(spec=FunctionCallContent)
tool_call_mock.split_name_dict.return_value = {"arg_name": "arg_value"}
tool_call_mock.to_kernel_arguments.return_value = {"arg_name": "arg_value"}
tool_call_mock.name = "test-function"
tool_call_mock.function_name = "function"
tool_call_mock.plugin_name = "test"
tool_call_mock.arguments = {"arg_name": "arg_value"}
tool_call_mock.ai_model_id = None
tool_call_mock.metadata = {}
tool_call_mock.index = 0
tool_call_mock.parse_arguments.return_value = {"arg_name": "arg_value"}
tool_call_mock.id = "test_id"
return tool_call_mock
@fixture(scope="function")
def chat_history() -> "ChatHistory":
from semantic_kernel.contents.chat_history import ChatHistory
return ChatHistory()
@fixture(scope="function")
def prompt() -> str:
return "test prompt"
# region: Connector Settings fixtures
@fixture
def exclude_list(request):
"""Fixture that returns a list of environment variables to exclude."""
return request.param if hasattr(request, "param") else []
@fixture
def override_env_param_dict(request):
"""Fixture that returns a dict of environment variables to override."""
return request.param if hasattr(request, "param") else {}
# These two fixtures are used for multiple things, also non-connector tests
@fixture()
def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for AzureOpenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment",
"AZURE_OPENAI_TEXT_DEPLOYMENT_NAME": "test_text_deployment",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME": "test_embedding_deployment",
"AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME": "test_text_to_image_deployment",
"AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME": "test_audio_to_text_deployment",
"AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME": "test_text_to_audio_deployment",
"AZURE_OPENAI_REALTIME_DEPLOYMENT_NAME": "test_realtime_deployment",
"AZURE_OPENAI_API_KEY": "test_api_key",
"AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com",
"AZURE_OPENAI_API_VERSION": "2023-03-15-preview",
"AZURE_OPENAI_BASE_URL": "https://test_text_deployment.test-base-url.com",
"AZURE_OPENAI_TOKEN_ENDPOINT": "https://test-token-endpoint.com",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@fixture()
def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for OpenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"OPENAI_API_KEY": "test_api_key",
"OPENAI_ORG_ID": "test_org_id",
"OPENAI_RESPONSES_MODEL_ID": "test_responses_model_id",
"OPENAI_CHAT_MODEL_ID": "test_chat_model_id",
"OPENAI_TEXT_MODEL_ID": "test_text_model_id",
"OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id",
"OPENAI_TEXT_TO_IMAGE_MODEL_ID": "test_text_to_image_model_id",
"OPENAI_AUDIO_TO_TEXT_MODEL_ID": "test_audio_to_text_model_id",
"OPENAI_TEXT_TO_AUDIO_MODEL_ID": "test_text_to_audio_model_id",
"OPENAI_REALTIME_MODEL_ID": "test_realtime_model_id",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
# region: Data Model Fixtures
# some of these fixtures are used in both unit and integration tests
@fixture
def index_kind(request) -> str:
if hasattr(request, "param"):
return request.param
return "hnsw"
@fixture
def distance_function(request) -> str:
if hasattr(request, "param"):
return request.param
return "cosine_similarity"
@fixture
def vector_property_type(request) -> str:
if hasattr(request, "param"):
return request.param
return "float"
@fixture
def dimensions(request) -> int:
if hasattr(request, "param"):
return request.param
return 5
@fixture
def dataclass_vector_data_model(
index_kind: str, distance_function: str, vector_property_type: str, dimensions: int
) -> object:
@vectorstoremodel
@dataclass
class MyDataModel:
vector: Annotated[
list[float] | str | None,
VectorStoreField(
"vector",
index_kind=index_kind,
dimensions=dimensions,
distance_function=distance_function,
type=vector_property_type,
),
] = None
id: Annotated[str, VectorStoreField("key", type="str")] = field(default_factory=lambda: str(uuid4()))
content: Annotated[str, VectorStoreField("data", type="str")] = "content1"
return MyDataModel
@fixture
def definition(
index_kind: str, distance_function: str, vector_property_type: str, dimensions: int
) -> VectorStoreCollectionDefinition:
return VectorStoreCollectionDefinition(
fields=[
VectorStoreField("key", name="id", type="str"),
VectorStoreField("data", name="content", type="str", is_full_text_indexed=True),
VectorStoreField(
"vector",
name="vector",
dimensions=dimensions,
index_kind=index_kind,
distance_function=distance_function,
type=vector_property_type,
),
]
)
@fixture
def definition_pandas(index_kind: str, distance_function: str, vector_property_type: str, dimensions: int) -> object:
import pandas as pd
return VectorStoreCollectionDefinition(
fields=[
VectorStoreField(
"vector",
name="vector",
index_kind=index_kind,
dimensions=dimensions,
distance_function=distance_function,
type=vector_property_type,
),
VectorStoreField("key", name="id"),
VectorStoreField("data", name="content", type="str"),
],
container_mode=True,
to_dict=lambda x: x.to_dict(orient="records"),
from_dict=lambda x, **_: pd.DataFrame(x),
)
@fixture
def record_type(index_kind: str, distance_function: str, vector_property_type: str, dimensions: int) -> object:
@vectorstoremodel
class DataModelClass(BaseModel):
content: Annotated[str, VectorStoreField("data")]
vector: Annotated[
list[float] | str | None,
VectorStoreField(
"vector",
type=vector_property_type,
dimensions=dimensions,
index_kind=index_kind,
distance_function=distance_function,
),
] = None
id: Annotated[str, VectorStoreField("key")]
def model_post_init(self, context: Any) -> None:
if self.vector is None:
self.vector = self.content
return DataModelClass
@fixture
def record_type_with_key_as_key_field(
index_kind: str, distance_function: str, vector_property_type: str, dimensions: int
) -> object:
"""Data model type with key as key field."""
@vectorstoremodel
class DataModelClass(BaseModel):
content: Annotated[str, VectorStoreField("data")]
vector: Annotated[
str | list[float] | None,
VectorStoreField(
"vector",
index_kind=index_kind,
distance_function=distance_function,
type=vector_property_type,
dimensions=dimensions,
),
]
key: Annotated[str, VectorStoreField("key")]
return DataModelClass
# region Declarative Spec
@register_agent_type("test_agent")
class TestAgent(DeclarativeSpecMixin, Agent):
@classmethod
def resolve_placeholders(cls, yaml_str, settings=None, extras=None):
return yaml_str
@classmethod
async def _from_dict(cls, data, **kwargs):
return cls(
name=data.get("name"),
description=data.get("description"),
instructions=data.get("instructions"),
kernel=data.get("kernel"),
)
async def get_response(self, messages, instructions_override=None):
return "test response"
async def invoke(self, messages, **kwargs):
return "invoke result"
async def invoke_stream(self, messages, **kwargs):
yield "stream result"
@fixture(scope="session")
def test_agent_cls():
return TestAgent
# endregion