mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
072123a8f1
- Foundry agent: add allow_preview=True to custom client test - Foundry hosting: raise max_output_tokens 50->200, add temperature, relax assertion in test_temperature_and_max_tokens - Foundry embedding: update skip reason with root cause (endpoint mismatch) - OpenAI file search: fix vector store indexing race condition by polling file_counts before querying; fix get_streaming_response -> get_response(stream=True) - Azure OpenAI file search: remove skip (transient 500 resolved) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
815 lines
28 KiB
Python
815 lines
28 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import os
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from agent_framework import (
|
|
AgentResponse,
|
|
AgentSession,
|
|
ChatContext,
|
|
ChatMiddleware,
|
|
ChatResponse,
|
|
ChatResponseUpdate,
|
|
Message,
|
|
tool,
|
|
)
|
|
from agent_framework_openai._chat_client import RawOpenAIChatClient
|
|
from azure.core.exceptions import ResourceNotFoundError
|
|
from azure.identity import AzureCliCredential
|
|
|
|
from agent_framework_foundry._agent import (
|
|
FoundryAgent,
|
|
RawFoundryAgent,
|
|
RawFoundryAgentChatClient,
|
|
_FoundryAgentChatClient,
|
|
)
|
|
|
|
skip_if_foundry_agent_integration_tests_disabled = pytest.mark.skipif(
|
|
os.getenv("FOUNDRY_PROJECT_ENDPOINT", "") in ("", "https://test-project.services.ai.azure.com/")
|
|
or os.getenv("FOUNDRY_AGENT_NAME", "") == "",
|
|
reason="No real FOUNDRY_PROJECT_ENDPOINT or FOUNDRY_AGENT_NAME provided; skipping integration tests.",
|
|
)
|
|
|
|
_FOUNDRY_AGENT_ENV_VARS = (
|
|
"FOUNDRY_PROJECT_ENDPOINT",
|
|
"FOUNDRY_AGENT_NAME",
|
|
"FOUNDRY_AGENT_VERSION",
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_foundry_agent_settings_env(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) -> None:
|
|
"""Prevent unit tests from inheriting Foundry agent settings from the shell."""
|
|
|
|
if request.node.get_closest_marker("integration") is not None:
|
|
return
|
|
|
|
for env_var in _FOUNDRY_AGENT_ENV_VARS:
|
|
monkeypatch.delenv(env_var, raising=False)
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_init_requires_agent_name() -> None:
|
|
"""Test that agent_name is required."""
|
|
|
|
with pytest.raises(ValueError, match="Agent name is required"):
|
|
RawFoundryAgentChatClient(
|
|
project_client=MagicMock(),
|
|
)
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_init_with_agent_name() -> None:
|
|
"""Test construction with agent_name and project_client without preview agent binding."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
agent_version="1.0",
|
|
)
|
|
|
|
assert client.agent_name == "test-agent"
|
|
assert client.agent_version == "1.0"
|
|
mock_project.get_openai_client.assert_called_once_with()
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_init_passes_agent_name_when_preview_enabled() -> None:
|
|
"""Test preview-enabled clients bind the OpenAI client to the agent endpoint."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="hosted-agent",
|
|
allow_preview=True,
|
|
default_headers={"x-test": "1"},
|
|
)
|
|
|
|
assert client.agent_name == "hosted-agent"
|
|
mock_project.get_openai_client.assert_called_once_with(
|
|
agent_name="hosted-agent",
|
|
default_headers={"x-test": "1"},
|
|
)
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_init_uses_explicit_parameters() -> None:
|
|
signature = inspect.signature(RawFoundryAgentChatClient.__init__)
|
|
|
|
assert "default_headers" in signature.parameters
|
|
assert "instruction_role" in signature.parameters
|
|
assert "compaction_strategy" in signature.parameters
|
|
assert "tokenizer" in signature.parameters
|
|
assert "additional_properties" in signature.parameters
|
|
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_as_agent_preserves_client_type() -> None:
|
|
"""Test that as_agent() wraps the client in FoundryAgent using the same client class."""
|
|
|
|
class CustomClient(RawFoundryAgentChatClient):
|
|
pass
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = CustomClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
agent_version="1.0",
|
|
)
|
|
|
|
agent = client.as_agent(instructions="You are helpful.")
|
|
|
|
assert isinstance(agent, FoundryAgent)
|
|
assert agent.name == "test-agent"
|
|
assert isinstance(agent.client, CustomClient)
|
|
assert agent.client.project_client is mock_project
|
|
assert agent.client.agent_name == "test-agent"
|
|
assert agent.client.agent_version == "1.0"
|
|
|
|
named_agent = client.as_agent(name="display-name", instructions="You are helpful.")
|
|
assert named_agent.name == "display-name"
|
|
assert named_agent.client.agent_name == "test-agent"
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_as_agent_uses_explicit_parameters() -> None:
|
|
signature = inspect.signature(RawFoundryAgentChatClient.as_agent)
|
|
|
|
assert "compaction_strategy" in signature.parameters
|
|
assert "tokenizer" in signature.parameters
|
|
assert "additional_properties" in signature.parameters
|
|
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
|
|
|
|
|
|
async def test_raw_foundry_agent_chat_client_prepare_options_validates_tools() -> None:
|
|
"""Test that _prepare_options rejects non-FunctionTool objects."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
with pytest.raises(TypeError, match="Only FunctionTool objects are accepted"):
|
|
await client._prepare_options(
|
|
messages=[Message(role="user", contents="hi")],
|
|
options={"tools": [{"type": "function", "function": {"name": "bad"}}]},
|
|
)
|
|
|
|
|
|
async def test_raw_foundry_agent_chat_client_prepare_options_accepts_function_tools() -> None:
|
|
"""Test that _prepare_options accepts FunctionTool objects."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_openai = MagicMock()
|
|
mock_project.get_openai_client.return_value = mock_openai
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
@tool(approval_mode="never_require")
|
|
def my_func() -> str:
|
|
"""A test function."""
|
|
|
|
return "ok"
|
|
|
|
with patch(
|
|
"agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options",
|
|
new_callable=AsyncMock,
|
|
return_value={},
|
|
):
|
|
result = await client._prepare_options(
|
|
messages=[Message(role="user", contents="hi")],
|
|
options={"tools": [my_func]},
|
|
)
|
|
|
|
assert result == {}
|
|
|
|
|
|
async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_fields() -> None:
|
|
"""Test that _prepare_options strips model and tool-loop fields from run_options."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_openai = MagicMock()
|
|
mock_project.get_openai_client.return_value = mock_openai
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
@tool(approval_mode="never_require")
|
|
def my_func() -> str:
|
|
"""A test function."""
|
|
|
|
return "ok"
|
|
|
|
with patch(
|
|
"agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options",
|
|
new_callable=AsyncMock,
|
|
return_value={
|
|
"model": "gpt-4.1",
|
|
"tools": [{"type": "function", "function": {"name": "my_func"}}],
|
|
"tool_choice": "auto",
|
|
"parallel_tool_calls": True,
|
|
},
|
|
):
|
|
result = await client._prepare_options(
|
|
messages=[Message(role="user", contents="hi")],
|
|
options={"tools": [my_func]},
|
|
)
|
|
|
|
assert "model" not in result
|
|
assert "tools" not in result
|
|
assert "tool_choice" not in result
|
|
assert "parallel_tool_calls" not in result
|
|
assert result == {}
|
|
|
|
|
|
async def test_raw_foundry_agent_chat_client_prepare_options_maps_agent_session_id_to_extra_body() -> None:
|
|
"""Test that service_session_id is forwarded as agent_session_id for hosted sessions."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_openai = MagicMock()
|
|
mock_project.get_openai_client.return_value = mock_openai
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
with patch(
|
|
"agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options",
|
|
new_callable=AsyncMock,
|
|
return_value={
|
|
"extra_body": {"custom": "value"},
|
|
"previous_response_id": "should-be-removed",
|
|
},
|
|
):
|
|
result = await client._prepare_options(
|
|
messages=[Message(role="user", contents="hi")],
|
|
options={"conversation_id": "agent-session-123", "isolation_key": "iso-key"},
|
|
)
|
|
|
|
assert result["extra_body"] == {
|
|
"custom": "value",
|
|
"agent_session_id": "agent-session-123",
|
|
}
|
|
assert "previous_response_id" not in result
|
|
assert "conversation" not in result
|
|
assert "isolation_key" not in result
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_parse_response_suppresses_conversation_id_for_agent_sessions() -> None:
|
|
"""Test that agent-session continuations do not overwrite session.service_session_id."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
parsed = ChatResponse(conversation_id="resp_123")
|
|
with patch(
|
|
"agent_framework_openai._chat_client.RawOpenAIChatClient._parse_response_from_openai",
|
|
return_value=parsed,
|
|
):
|
|
result = client._parse_response_from_openai(
|
|
response=MagicMock(),
|
|
options={"conversation_id": "agent-session-123"},
|
|
)
|
|
|
|
assert result.conversation_id is None
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_for_agent_sessions() -> None:
|
|
"""Test that agent-session stream updates do not overwrite session.service_session_id."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
parsed = ChatResponseUpdate(conversation_id="resp_123")
|
|
with patch(
|
|
"agent_framework_openai._chat_client.RawOpenAIChatClient._parse_chunk_from_openai",
|
|
return_value=parsed,
|
|
):
|
|
result = client._parse_chunk_from_openai(
|
|
event=MagicMock(type="response.output_text.delta"),
|
|
options={"conversation_id": "agent-session-123"},
|
|
function_call_ids={},
|
|
)
|
|
|
|
assert result.conversation_id is None
|
|
|
|
|
|
def test_raw_foundry_agent_chat_client_check_model_presence_is_noop() -> None:
|
|
"""Test that _check_model_presence does nothing (model is on service)."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
options: dict[str, Any] = {}
|
|
client._check_model_presence(options)
|
|
assert "model" not in options
|
|
|
|
|
|
def test_foundry_agent_chat_client_init() -> None:
|
|
"""Test construction of the full-middleware client."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = _FoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
agent_version="1.0",
|
|
)
|
|
|
|
assert client.agent_name == "test-agent"
|
|
|
|
|
|
def test_foundry_agent_chat_client_init_uses_explicit_parameters() -> None:
|
|
signature = inspect.signature(_FoundryAgentChatClient.__init__)
|
|
|
|
assert "default_headers" in signature.parameters
|
|
assert "instruction_role" in signature.parameters
|
|
assert "compaction_strategy" in signature.parameters
|
|
assert "tokenizer" in signature.parameters
|
|
assert "additional_properties" in signature.parameters
|
|
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
|
|
|
|
|
|
def test_raw_foundry_agent_init_creates_client() -> None:
|
|
"""Test that RawFoundryAgent creates a client internally."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
agent = RawFoundryAgent(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
agent_version="1.0",
|
|
)
|
|
|
|
assert agent.client is not None
|
|
assert agent.client.agent_name == "test-agent"
|
|
|
|
|
|
def test_raw_foundry_agent_init_with_custom_client_type() -> None:
|
|
"""Test that client_type parameter is respected."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
agent = RawFoundryAgent(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
client_type=RawFoundryAgentChatClient,
|
|
)
|
|
|
|
assert isinstance(agent.client, RawFoundryAgentChatClient)
|
|
|
|
|
|
def test_raw_foundry_agent_init_uses_explicit_parameters() -> None:
|
|
signature = inspect.signature(RawFoundryAgent.__init__)
|
|
|
|
assert "instructions" in signature.parameters
|
|
assert "default_options" in signature.parameters
|
|
assert "compaction_strategy" in signature.parameters
|
|
assert "tokenizer" in signature.parameters
|
|
assert "additional_properties" in signature.parameters
|
|
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
|
|
|
|
|
|
def test_foundry_agent_init_uses_explicit_parameters() -> None:
|
|
signature = inspect.signature(FoundryAgent.__init__)
|
|
|
|
assert "instructions" in signature.parameters
|
|
assert "default_options" in signature.parameters
|
|
assert "compaction_strategy" in signature.parameters
|
|
assert "tokenizer" in signature.parameters
|
|
assert "additional_properties" in signature.parameters
|
|
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
|
|
|
|
|
|
def test_raw_foundry_agent_init_rejects_invalid_client_type() -> None:
|
|
"""Test that invalid client_type raises TypeError."""
|
|
|
|
with pytest.raises(TypeError, match="must be a subclass of RawFoundryAgentChatClient"):
|
|
RawFoundryAgent(
|
|
project_client=MagicMock(),
|
|
agent_name="test-agent",
|
|
client_type=object, # type: ignore[arg-type]
|
|
)
|
|
|
|
|
|
def test_raw_foundry_agent_init_with_function_tools() -> None:
|
|
"""Test that FunctionTool and callables are accepted."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
@tool(approval_mode="never_require")
|
|
def my_func() -> str:
|
|
"""A test function."""
|
|
|
|
return "ok"
|
|
|
|
agent = RawFoundryAgent(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
tools=[my_func],
|
|
)
|
|
|
|
assert agent.default_options.get("tools") is not None
|
|
|
|
|
|
async def test_raw_foundry_agent_prepare_run_context_creates_service_session_from_isolation_key() -> None:
|
|
"""Test that RawFoundryAgent lazily creates a hosted session and stores it on service_session_id."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
mock_project.beta = SimpleNamespace(
|
|
agents=SimpleNamespace(
|
|
create_session=AsyncMock(return_value=SimpleNamespace(agent_session_id="agent-session-123"))
|
|
)
|
|
)
|
|
|
|
agent = RawFoundryAgent(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
agent_version="1.0",
|
|
allow_preview=True,
|
|
)
|
|
session = AgentSession()
|
|
|
|
with patch(
|
|
"agent_framework._agents.RawAgent._prepare_run_context",
|
|
new=AsyncMock(return_value={"ok": True}),
|
|
) as mock_prepare_run_context:
|
|
result = await agent._prepare_run_context(
|
|
messages="hi",
|
|
session=session,
|
|
tools=None,
|
|
options={"isolation_key": "iso-key"},
|
|
compaction_strategy=None,
|
|
tokenizer=None,
|
|
function_invocation_kwargs=None,
|
|
client_kwargs=None,
|
|
)
|
|
|
|
assert result == {"ok": True}
|
|
assert session.service_session_id == "agent-session-123"
|
|
mock_project.beta.agents.create_session.assert_awaited_once()
|
|
create_session_kwargs = mock_project.beta.agents.create_session.await_args.kwargs
|
|
assert create_session_kwargs["agent_name"] == "test-agent"
|
|
assert create_session_kwargs["isolation_key"] == "iso-key"
|
|
assert "version_indicator" in create_session_kwargs
|
|
mock_prepare_run_context.assert_awaited_once()
|
|
|
|
|
|
async def test_raw_foundry_agent_prepare_run_context_requires_preview_for_hosted_sessions() -> None:
|
|
"""Test that hosted-agent sessions require allow_preview=True."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
agent = RawFoundryAgent(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="allow_preview=True"):
|
|
await agent._prepare_run_context(
|
|
messages="hi",
|
|
session=AgentSession(),
|
|
tools=None,
|
|
options={"isolation_key": "iso-key"},
|
|
compaction_strategy=None,
|
|
tokenizer=None,
|
|
function_invocation_kwargs=None,
|
|
client_kwargs=None,
|
|
)
|
|
|
|
|
|
def test_foundry_agent_init() -> None:
|
|
"""Test construction of the full-middleware agent."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
agent = FoundryAgent(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
agent_version="1.0",
|
|
)
|
|
|
|
assert agent.client is not None
|
|
assert agent.client.agent_name == "test-agent"
|
|
|
|
|
|
def test_foundry_agent_init_with_middleware() -> None:
|
|
"""Test that agent-level middleware is accepted."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
class MyMiddleware(ChatMiddleware):
|
|
async def process(self, context: ChatContext) -> None:
|
|
pass
|
|
|
|
agent = FoundryAgent(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
middleware=[MyMiddleware()],
|
|
)
|
|
|
|
assert agent.client is not None
|
|
|
|
|
|
async def test_foundry_agent_configure_azure_monitor() -> None:
|
|
"""Test configure_azure_monitor delegates through the underlying client."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
mock_project.telemetry.get_application_insights_connection_string = AsyncMock(
|
|
return_value="InstrumentationKey=test-key;IngestionEndpoint=https://test.endpoint"
|
|
)
|
|
agent = FoundryAgent(project_client=mock_project, agent_name="test-agent")
|
|
|
|
mock_configure = MagicMock()
|
|
mock_views = MagicMock(return_value=[])
|
|
mock_resource = MagicMock()
|
|
mock_enable = MagicMock()
|
|
|
|
with (
|
|
patch.dict(
|
|
"sys.modules",
|
|
{"azure.monitor.opentelemetry": MagicMock(configure_azure_monitor=mock_configure)},
|
|
),
|
|
patch("agent_framework.observability.create_metric_views", mock_views),
|
|
patch("agent_framework.observability.create_resource", return_value=mock_resource),
|
|
patch("agent_framework.observability.enable_instrumentation", mock_enable),
|
|
):
|
|
await agent.configure_azure_monitor(enable_sensitive_data=True)
|
|
|
|
mock_project.telemetry.get_application_insights_connection_string.assert_called_once()
|
|
call_kwargs = mock_configure.call_args.kwargs
|
|
assert call_kwargs["connection_string"] == "InstrumentationKey=test-key;IngestionEndpoint=https://test.endpoint"
|
|
assert call_kwargs["views"] == []
|
|
assert call_kwargs["resource"] is mock_resource
|
|
mock_enable.assert_called_once_with(enable_sensitive_data=True)
|
|
|
|
|
|
async def test_foundry_agent_configure_azure_monitor_resource_not_found() -> None:
|
|
"""Test configure_azure_monitor handles ResourceNotFoundError gracefully."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
mock_project.telemetry.get_application_insights_connection_string = AsyncMock(
|
|
side_effect=ResourceNotFoundError("No Application Insights found")
|
|
)
|
|
agent = FoundryAgent(project_client=mock_project, agent_name="test-agent")
|
|
|
|
await agent.configure_azure_monitor()
|
|
|
|
mock_project.telemetry.get_application_insights_connection_string.assert_called_once()
|
|
|
|
|
|
async def test_foundry_agent_configure_azure_monitor_import_error() -> None:
|
|
"""Test configure_azure_monitor raises ImportError when Azure Monitor is unavailable."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
mock_project.telemetry.get_application_insights_connection_string = AsyncMock(
|
|
return_value="InstrumentationKey=test-key"
|
|
)
|
|
agent = FoundryAgent(project_client=mock_project, agent_name="test-agent")
|
|
original_import = __import__
|
|
|
|
def _import_with_missing_azure_monitor(
|
|
name: str,
|
|
globals: dict[str, Any] | None = None,
|
|
locals: dict[str, Any] | None = None,
|
|
fromlist: tuple[str, ...] = (),
|
|
level: int = 0,
|
|
) -> Any:
|
|
if name == "azure.monitor.opentelemetry":
|
|
raise ImportError("No module named 'azure.monitor.opentelemetry'")
|
|
return original_import(name, globals, locals, fromlist, level)
|
|
|
|
with (
|
|
patch.dict(sys.modules, {"azure.monitor.opentelemetry": None}),
|
|
patch("builtins.__import__", side_effect=_import_with_missing_azure_monitor),
|
|
pytest.raises(ImportError, match="azure-monitor-opentelemetry is required"),
|
|
):
|
|
await agent.configure_azure_monitor()
|
|
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.integration
|
|
@skip_if_foundry_agent_integration_tests_disabled
|
|
async def test_foundry_agent_basic_run() -> None:
|
|
"""Smoke-test FoundryAgent against a real configured agent."""
|
|
async with FoundryAgent(credential=AzureCliCredential(), allow_preview=True) as agent:
|
|
response = await agent.run("Please respond with exactly: 'This is a response test.'")
|
|
|
|
assert isinstance(response, AgentResponse)
|
|
assert response.text is not None
|
|
assert "response test" in response.text.lower()
|
|
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.integration
|
|
@skip_if_foundry_agent_integration_tests_disabled
|
|
async def test_foundry_agent_custom_client_run() -> None:
|
|
"""Smoke-test FoundryAgent against a real configured agent."""
|
|
async with FoundryAgent(
|
|
credential=AzureCliCredential(), client_type=RawFoundryAgentChatClient, allow_preview=True
|
|
) as agent:
|
|
response = await agent.run("Please respond with exactly: 'This is a response test.'")
|
|
|
|
assert isinstance(response, AgentResponse)
|
|
assert response.text is not None
|
|
assert "response test" in response.text.lower()
|
|
|
|
|
|
def test_parse_chunk_surfaces_oauth_consent_request() -> None:
|
|
"""An oauth_consent_request output item surfaces as Content with consent_link."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
mock_event = MagicMock()
|
|
mock_event.type = "response.output_item.added"
|
|
mock_item = MagicMock()
|
|
mock_item.type = "oauth_consent_request"
|
|
mock_item.consent_link = "https://consent-host.example.com/login?data=abc123"
|
|
mock_item.id = "oauth-item-1"
|
|
mock_event.item = mock_item
|
|
mock_event.output_index = 0
|
|
|
|
update = client._parse_chunk_from_openai(mock_event, {}, {})
|
|
|
|
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
|
|
assert len(consent_contents) == 1
|
|
assert consent_contents[0].consent_link == "https://consent-host.example.com/login?data=abc123"
|
|
assert update.role == "assistant"
|
|
assert update.raw_representation is mock_event
|
|
|
|
|
|
def test_parse_chunk_skips_non_https_oauth_consent() -> None:
|
|
"""An oauth_consent_request with a non-HTTPS link is rejected."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
mock_event = MagicMock()
|
|
mock_event.type = "response.output_item.added"
|
|
mock_item = MagicMock()
|
|
mock_item.type = "oauth_consent_request"
|
|
mock_item.consent_link = "http://insecure.example.com/login"
|
|
mock_item.id = "oauth-item-2"
|
|
mock_event.item = mock_item
|
|
mock_event.output_index = 0
|
|
|
|
update = client._parse_chunk_from_openai(mock_event, {}, {})
|
|
|
|
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
|
|
assert len(consent_contents) == 0
|
|
|
|
|
|
def test_parse_chunk_handles_missing_consent_link() -> None:
|
|
"""An oauth_consent_request without a consent_link produces no content."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
mock_event = MagicMock()
|
|
mock_event.type = "response.output_item.added"
|
|
mock_item = MagicMock()
|
|
mock_item.type = "oauth_consent_request"
|
|
mock_item.consent_link = None
|
|
mock_item.id = "oauth-item-3"
|
|
mock_event.item = mock_item
|
|
mock_event.output_index = 0
|
|
|
|
update = client._parse_chunk_from_openai(mock_event, {}, {})
|
|
|
|
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
|
|
assert len(consent_contents) == 0
|
|
|
|
|
|
def test_parse_chunk_handles_empty_string_consent_link() -> None:
|
|
"""An oauth_consent_request with empty-string consent_link produces no content."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
mock_event = MagicMock()
|
|
mock_event.type = "response.output_item.added"
|
|
mock_item = MagicMock()
|
|
mock_item.type = "oauth_consent_request"
|
|
mock_item.consent_link = ""
|
|
mock_item.id = "oauth-item-4"
|
|
mock_event.item = mock_item
|
|
mock_event.output_index = 0
|
|
|
|
update = client._parse_chunk_from_openai(mock_event, {}, {})
|
|
|
|
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
|
|
assert len(consent_contents) == 0
|
|
|
|
|
|
def test_parse_chunk_delegates_non_oauth_events_to_super() -> None:
|
|
"""Non-oauth events are delegated to super()._parse_chunk_from_openai()."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
mock_event = MagicMock()
|
|
mock_event.type = "response.output_text.delta"
|
|
|
|
with patch.object(
|
|
RawOpenAIChatClient,
|
|
"_parse_chunk_from_openai",
|
|
return_value=MagicMock(),
|
|
) as mock_super:
|
|
client._parse_chunk_from_openai(mock_event, {}, {})
|
|
mock_super.assert_called_once_with(mock_event, {}, {}, None)
|
|
|
|
|
|
def test_parse_chunk_surfaces_oauth_consent_requested_event() -> None:
|
|
"""A top-level response.oauth_consent_requested event surfaces as Content."""
|
|
|
|
mock_project = MagicMock()
|
|
mock_project.get_openai_client.return_value = MagicMock()
|
|
|
|
client = RawFoundryAgentChatClient(
|
|
project_client=mock_project,
|
|
agent_name="test-agent",
|
|
)
|
|
|
|
mock_event = MagicMock()
|
|
mock_event.type = "response.oauth_consent_requested"
|
|
mock_event.consent_link = "https://consent-host.example.com/authorize?code=xyz"
|
|
mock_event.id = "consent-event-1"
|
|
|
|
update = client._parse_chunk_from_openai(mock_event, {}, {})
|
|
|
|
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
|
|
assert len(consent_contents) == 1
|
|
assert consent_contents[0].consent_link == "https://consent-host.example.com/authorize?code=xyz"
|
|
assert update.role == "assistant"
|
|
assert update.raw_representation is mock_event
|