chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from microsoft_agents.copilotstudio.client import CopilotClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def exclude_list(request: Any) -> list[str]:
|
||||
"""Fixture that returns a list of environment variables to exclude."""
|
||||
return request.param if hasattr(request, "param") else []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def override_env_param_dict(request: Any) -> dict[str, str]:
|
||||
"""Fixture that returns a dict of environment variables to override."""
|
||||
return request.param if hasattr(request, "param") else {}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def copilot_studio_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore
|
||||
"""Fixture to set environment variables for CopilotStudioSettings."""
|
||||
|
||||
if exclude_list is None:
|
||||
exclude_list = []
|
||||
|
||||
if override_env_param_dict is None:
|
||||
override_env_param_dict = {}
|
||||
|
||||
env_vars = {
|
||||
"COPILOTSTUDIOAGENT__ENVIRONMENTID": "test-environment-id",
|
||||
"COPILOTSTUDIOAGENT__SCHEMANAME": "test-schema-name",
|
||||
"COPILOTSTUDIOAGENT__AGENTAPPID": "test-client-id",
|
||||
"COPILOTSTUDIOAGENT__TENANTID": "test-tenant-id",
|
||||
}
|
||||
|
||||
env_vars.update(override_env_param_dict) # type: ignore
|
||||
|
||||
for key, value in env_vars.items():
|
||||
if key in exclude_list:
|
||||
monkeypatch.delenv(key, raising=False) # type: ignore
|
||||
continue
|
||||
monkeypatch.setenv(key, value) # type: ignore
|
||||
|
||||
return env_vars
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_copilot_client() -> MagicMock:
|
||||
"""Mock CopilotClient for testing."""
|
||||
return MagicMock(spec=CopilotClient)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pca() -> MagicMock:
|
||||
"""Mock PublicClientApplication for testing."""
|
||||
mock_pca = MagicMock()
|
||||
|
||||
# Mock successful token response
|
||||
mock_token_response = {
|
||||
"access_token": "test-access-token-12345",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
|
||||
mock_pca.get_accounts.return_value = []
|
||||
mock_pca.acquire_token_interactive.return_value = mock_token_response
|
||||
mock_pca.acquire_token_silent.return_value = mock_token_response
|
||||
|
||||
return mock_pca
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_activity() -> MagicMock:
|
||||
"""Mock Activity for testing."""
|
||||
mock_activity = MagicMock()
|
||||
mock_activity.text = "Test response"
|
||||
mock_activity.type = "message"
|
||||
mock_activity.id = "test-activity-id"
|
||||
mock_activity.from_property.name = "Test Bot"
|
||||
return mock_activity
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_conversation() -> MagicMock:
|
||||
"""Mock conversation for testing."""
|
||||
mock_conversation = MagicMock()
|
||||
mock_conversation.id = "test-conversation-id"
|
||||
return mock_conversation
|
||||
@@ -0,0 +1,243 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework.exceptions import AgentException
|
||||
|
||||
from agent_framework_copilotstudio._acquire_token import DEFAULT_SCOPES, acquire_token
|
||||
|
||||
|
||||
class TestAcquireToken:
|
||||
"""Test class for token acquisition functionality."""
|
||||
|
||||
def test_acquire_token_missing_client_id(self) -> None:
|
||||
"""Test that acquire_token raises ValueError when client_id is missing."""
|
||||
with pytest.raises(ValueError, match="Client ID is required for token acquisition"):
|
||||
acquire_token(client_id="", tenant_id="test-tenant-id")
|
||||
|
||||
def test_acquire_token_missing_tenant_id(self) -> None:
|
||||
"""Test that acquire_token raises ValueError when tenant_id is missing."""
|
||||
with pytest.raises(ValueError, match="Tenant ID is required for token acquisition"):
|
||||
acquire_token(client_id="test-client-id", tenant_id="")
|
||||
|
||||
def test_acquire_token_none_client_id(self) -> None:
|
||||
"""Test that acquire_token raises ValueError when client_id is None."""
|
||||
with pytest.raises(ValueError, match="Client ID is required for token acquisition"):
|
||||
acquire_token(client_id=None, tenant_id="test-tenant-id") # type: ignore
|
||||
|
||||
def test_acquire_token_none_tenant_id(self) -> None:
|
||||
"""Test that acquire_token raises ValueError when tenant_id is None."""
|
||||
with pytest.raises(ValueError, match="Tenant ID is required for token acquisition"):
|
||||
acquire_token(client_id="test-client-id", tenant_id=None) # type: ignore
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_silent_success(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test successful silent token acquisition."""
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_account = MagicMock()
|
||||
mock_pca.get_accounts.return_value = [mock_account]
|
||||
|
||||
mock_token_response = {"access_token": "test-access-token-12345"}
|
||||
mock_pca.acquire_token_silent.return_value = mock_token_response
|
||||
|
||||
result = acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
)
|
||||
|
||||
assert result == "test-access-token-12345"
|
||||
mock_pca_class.assert_called_once_with(
|
||||
client_id="test-client-id",
|
||||
authority="https://login.microsoftonline.com/test-tenant-id",
|
||||
token_cache=None,
|
||||
)
|
||||
mock_pca.get_accounts.assert_called_once_with(username=None)
|
||||
mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_silent_success_with_username(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test successful silent token acquisition with username."""
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_account = MagicMock()
|
||||
mock_pca.get_accounts.return_value = [mock_account]
|
||||
|
||||
mock_token_response = {"access_token": "test-access-token-12345"}
|
||||
mock_pca.acquire_token_silent.return_value = mock_token_response
|
||||
|
||||
result = acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
username="test-user@example.com",
|
||||
)
|
||||
|
||||
assert result == "test-access-token-12345"
|
||||
mock_pca.get_accounts.assert_called_once_with(username="test-user@example.com")
|
||||
mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_silent_success_with_custom_scopes(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test successful silent token acquisition with custom scopes."""
|
||||
# Setup
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_account = MagicMock()
|
||||
mock_pca.get_accounts.return_value = [mock_account]
|
||||
|
||||
mock_token_response = {"access_token": "test-access-token-12345"}
|
||||
mock_pca.acquire_token_silent.return_value = mock_token_response
|
||||
|
||||
custom_scopes = ["https://custom.api.com/.default"]
|
||||
|
||||
result = acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
scopes=custom_scopes,
|
||||
)
|
||||
|
||||
assert result == "test-access-token-12345"
|
||||
mock_pca.acquire_token_silent.assert_called_once_with(scopes=custom_scopes, account=mock_account)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_interactive_success_no_accounts(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test successful interactive token acquisition when no cached accounts exist."""
|
||||
# Setup
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_pca.get_accounts.return_value = [] # No cached accounts
|
||||
|
||||
mock_token_response = {"access_token": "test-interactive-token-67890"}
|
||||
mock_pca.acquire_token_interactive.return_value = mock_token_response
|
||||
|
||||
result = acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
)
|
||||
|
||||
assert result == "test-interactive-token-67890"
|
||||
mock_pca.acquire_token_interactive.assert_called_once_with(scopes=DEFAULT_SCOPES)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_fallback_to_interactive_after_silent_fails(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test fallback to interactive authentication when silent acquisition fails."""
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_account = MagicMock()
|
||||
mock_pca.get_accounts.return_value = [mock_account]
|
||||
|
||||
# Silent acquisition fails with error response
|
||||
mock_silent_error_response = {"error": "invalid_grant", "error_description": "Token expired"}
|
||||
mock_pca.acquire_token_silent.return_value = mock_silent_error_response
|
||||
|
||||
# Interactive acquisition succeeds
|
||||
mock_interactive_response = {"access_token": "test-interactive-token-67890"}
|
||||
mock_pca.acquire_token_interactive.return_value = mock_interactive_response
|
||||
|
||||
result = acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
)
|
||||
|
||||
assert result == "test-interactive-token-67890"
|
||||
mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account)
|
||||
mock_pca.acquire_token_interactive.assert_called_once_with(scopes=DEFAULT_SCOPES)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_fallback_to_interactive_after_silent_exception(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test fallback to interactive authentication when silent acquisition throws exception."""
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_account = MagicMock()
|
||||
mock_pca.get_accounts.return_value = [mock_account]
|
||||
|
||||
# Silent acquisition throws exception
|
||||
mock_pca.acquire_token_silent.side_effect = Exception("Network error")
|
||||
|
||||
# Interactive acquisition succeeds
|
||||
mock_interactive_response = {"access_token": "test-interactive-token-67890"}
|
||||
mock_pca.acquire_token_interactive.return_value = mock_interactive_response
|
||||
|
||||
result = acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
)
|
||||
|
||||
assert result == "test-interactive-token-67890"
|
||||
mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account)
|
||||
mock_pca.acquire_token_interactive.assert_called_once_with(scopes=DEFAULT_SCOPES)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_interactive_error_response(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test that acquire_token handles error responses from interactive authentication."""
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_pca.get_accounts.return_value = [] # No cached accounts
|
||||
|
||||
# Interactive acquisition returns error
|
||||
mock_error_response = {"error": "access_denied", "error_description": "User denied consent"}
|
||||
mock_pca.acquire_token_interactive.return_value = mock_error_response
|
||||
|
||||
with pytest.raises(AgentException, match="Authentication token cannot be acquired"):
|
||||
acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_interactive_exception(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test that acquire_token handles exceptions from interactive authentication."""
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_pca.get_accounts.return_value = [] # No cached accounts
|
||||
|
||||
# Interactive acquisition throws exception
|
||||
mock_pca.acquire_token_interactive.side_effect = Exception("Authentication service unavailable")
|
||||
|
||||
with pytest.raises(AgentException, match="Failed to acquire authentication token"):
|
||||
acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication")
|
||||
def test_acquire_token_with_token_cache(self, mock_pca_class: MagicMock) -> None:
|
||||
"""Test acquire_token with custom token cache."""
|
||||
mock_pca = MagicMock()
|
||||
mock_pca_class.return_value = mock_pca
|
||||
|
||||
mock_account = MagicMock()
|
||||
mock_pca.get_accounts.return_value = [mock_account]
|
||||
|
||||
mock_token_response = {"access_token": "test-cached-token"}
|
||||
mock_pca.acquire_token_silent.return_value = mock_token_response
|
||||
|
||||
mock_token_cache = MagicMock()
|
||||
|
||||
result = acquire_token(
|
||||
client_id="test-client-id",
|
||||
tenant_id="test-tenant-id",
|
||||
token_cache=mock_token_cache,
|
||||
)
|
||||
|
||||
assert result == "test-cached-token"
|
||||
mock_pca_class.assert_called_once_with(
|
||||
client_id="test-client-id",
|
||||
authority="https://login.microsoftonline.com/test-tenant-id",
|
||||
token_cache=mock_token_cache,
|
||||
)
|
||||
|
||||
def test_default_scopes_constant(self) -> None:
|
||||
"""Test that DEFAULT_SCOPES constant is properly defined."""
|
||||
assert DEFAULT_SCOPES == ["https://api.powerplatform.com/.default"]
|
||||
assert isinstance(DEFAULT_SCOPES, list)
|
||||
assert len(DEFAULT_SCOPES) == 1
|
||||
@@ -0,0 +1,361 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, AgentSession, Content, Message
|
||||
from agent_framework.exceptions import AgentException
|
||||
from microsoft_agents.copilotstudio.client import CopilotClient
|
||||
|
||||
from agent_framework_copilotstudio import CopilotStudioAgent
|
||||
|
||||
|
||||
def create_async_generator(items: list[Any]) -> Any:
|
||||
"""Helper to create async generator mock."""
|
||||
|
||||
async def async_gen() -> Any:
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
return async_gen()
|
||||
|
||||
|
||||
class TestCopilotStudioAgent:
|
||||
"""Test cases for CopilotStudioAgent."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_activity(self) -> MagicMock:
|
||||
activity = MagicMock()
|
||||
activity.text = "Test response"
|
||||
activity.type = "message"
|
||||
activity.id = "test-id"
|
||||
activity.from_property.name = "Test Bot"
|
||||
return activity
|
||||
|
||||
@pytest.fixture
|
||||
def mock_copilot_client(self) -> MagicMock:
|
||||
return MagicMock(spec=CopilotClient)
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.acquire_token")
|
||||
@patch("agent_framework_copilotstudio._agent.load_settings")
|
||||
def test_init_missing_environment_id(self, mock_load_settings: MagicMock, mock_acquire_token: MagicMock) -> None:
|
||||
mock_acquire_token.return_value = "fake-token"
|
||||
mock_load_settings.return_value = {
|
||||
"environmentid": None,
|
||||
"schemaname": "test-bot",
|
||||
"tenantid": "test-tenant",
|
||||
"agentappid": "test-client",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="environment ID is required"):
|
||||
CopilotStudioAgent()
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.acquire_token")
|
||||
@patch("agent_framework_copilotstudio._agent.load_settings")
|
||||
def test_init_missing_bot_id(self, mock_load_settings: MagicMock, mock_acquire_token: MagicMock) -> None:
|
||||
mock_acquire_token.return_value = "fake-token"
|
||||
mock_load_settings.return_value = {
|
||||
"environmentid": "test-env",
|
||||
"schemaname": None,
|
||||
"tenantid": "test-tenant",
|
||||
"agentappid": "test-client",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="agent identifier"):
|
||||
CopilotStudioAgent()
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.acquire_token")
|
||||
@patch("agent_framework_copilotstudio._agent.load_settings")
|
||||
def test_init_missing_tenant_id(self, mock_load_settings: MagicMock, mock_acquire_token: MagicMock) -> None:
|
||||
mock_acquire_token.return_value = "fake-token"
|
||||
mock_load_settings.return_value = {
|
||||
"environmentid": "test-env",
|
||||
"schemaname": "test-bot",
|
||||
"tenantid": None,
|
||||
"agentappid": "test-client",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="tenant ID is required"):
|
||||
CopilotStudioAgent()
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.acquire_token")
|
||||
@patch("agent_framework_copilotstudio._agent.load_settings")
|
||||
def test_init_missing_client_id(self, mock_load_settings: MagicMock, mock_acquire_token: MagicMock) -> None:
|
||||
mock_acquire_token.return_value = "fake-token"
|
||||
mock_load_settings.return_value = {
|
||||
"environmentid": "test-env",
|
||||
"schemaname": "test-bot",
|
||||
"tenantid": "test-tenant",
|
||||
"agentappid": None,
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="client ID is required"):
|
||||
CopilotStudioAgent()
|
||||
|
||||
def test_init_with_client(self, mock_copilot_client: MagicMock) -> None:
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
assert agent.client == mock_copilot_client
|
||||
assert agent.id is not None
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.acquire_token")
|
||||
def test_init_empty_environment_id(self, mock_acquire_token: MagicMock) -> None:
|
||||
mock_acquire_token.return_value = "fake-token"
|
||||
with patch("agent_framework_copilotstudio._agent.load_settings") as mock_load_settings:
|
||||
mock_load_settings.return_value = {
|
||||
"environmentid": "",
|
||||
"schemaname": "test-bot",
|
||||
"tenantid": "test-tenant",
|
||||
"agentappid": "test-client",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="environment ID is required"):
|
||||
CopilotStudioAgent()
|
||||
|
||||
@patch("agent_framework_copilotstudio._acquire_token.acquire_token")
|
||||
def test_init_empty_schema_name(self, mock_acquire_token: MagicMock) -> None:
|
||||
mock_acquire_token.return_value = "fake-token"
|
||||
with patch("agent_framework_copilotstudio._agent.load_settings") as mock_load_settings:
|
||||
mock_load_settings.return_value = {
|
||||
"environmentid": "test-env",
|
||||
"schemaname": "",
|
||||
"tenantid": "test-tenant",
|
||||
"agentappid": "test-client",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="agent identifier"):
|
||||
CopilotStudioAgent()
|
||||
|
||||
async def test_run_with_string_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
|
||||
"""Test run method with string message."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity])
|
||||
|
||||
response = await agent.run("test message")
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert len(response.messages) == 1
|
||||
content = response.messages[0].contents[0]
|
||||
assert content.type == "text"
|
||||
assert content.text == "Test response"
|
||||
assert response.messages[0].role == "assistant"
|
||||
|
||||
async def test_run_with_chat_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
|
||||
"""Test run method with Message."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity])
|
||||
|
||||
chat_message = Message(role="user", contents=[Content.from_text("test message")])
|
||||
response = await agent.run(chat_message)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert len(response.messages) == 1
|
||||
content = response.messages[0].contents[0]
|
||||
assert content.type == "text"
|
||||
assert content.text == "Test response"
|
||||
assert response.messages[0].role == "assistant"
|
||||
|
||||
async def test_run_with_session(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
|
||||
"""Test run method with existing session."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
session = AgentSession()
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity])
|
||||
|
||||
response = await agent.run("test message", session=session)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert len(response.messages) == 1
|
||||
assert session.service_session_id == "test-conversation-id"
|
||||
|
||||
async def test_run_start_conversation_failure(self, mock_copilot_client: MagicMock) -> None:
|
||||
"""Test run method when conversation start fails."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([])
|
||||
|
||||
with pytest.raises(AgentException, match="Failed to start a new conversation"):
|
||||
await agent.run("test message")
|
||||
|
||||
async def test_run_streaming_with_string_message(self, mock_copilot_client: MagicMock) -> None:
|
||||
"""Test run(stream=True) method with string message."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
typing_activity = MagicMock()
|
||||
typing_activity.text = "Streaming response"
|
||||
typing_activity.type = "typing"
|
||||
typing_activity.id = "test-typing-id"
|
||||
typing_activity.from_property.name = "Test Bot"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([typing_activity])
|
||||
|
||||
response_count = 0
|
||||
async for response in agent.run("test message", stream=True):
|
||||
assert isinstance(response, AgentResponseUpdate)
|
||||
content = response.contents[0]
|
||||
assert content.type == "text"
|
||||
assert content.text == "Streaming response"
|
||||
response_count += 1
|
||||
|
||||
assert response_count == 1
|
||||
|
||||
async def test_run_streaming_with_session(self, mock_copilot_client: MagicMock) -> None:
|
||||
"""Test run(stream=True) method with existing session."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
session = AgentSession()
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
typing_activity = MagicMock()
|
||||
typing_activity.text = "Streaming response"
|
||||
typing_activity.type = "typing"
|
||||
typing_activity.id = "test-typing-id"
|
||||
typing_activity.from_property.name = "Test Bot"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([typing_activity])
|
||||
|
||||
response_count = 0
|
||||
async for response in agent.run("test message", session=session, stream=True):
|
||||
assert isinstance(response, AgentResponseUpdate)
|
||||
content = response.contents[0]
|
||||
assert content.type == "text"
|
||||
assert content.text == "Streaming response"
|
||||
response_count += 1
|
||||
|
||||
assert response_count == 1
|
||||
assert session.service_session_id == "test-conversation-id"
|
||||
|
||||
async def test_run_reuses_existing_conversation(
|
||||
self, mock_copilot_client: MagicMock, mock_activity: MagicMock
|
||||
) -> None:
|
||||
"""Test run method reuses an existing conversation ID from the session."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
session = AgentSession()
|
||||
session.service_session_id = "existing-conversation-id"
|
||||
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity])
|
||||
|
||||
response = await agent.run("test message", session=session)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert session.service_session_id == "existing-conversation-id"
|
||||
mock_copilot_client.start_conversation.assert_not_called()
|
||||
mock_copilot_client.ask_question.assert_called_once_with("test message", "existing-conversation-id")
|
||||
|
||||
async def test_run_streaming_reuses_existing_conversation(self, mock_copilot_client: MagicMock) -> None:
|
||||
"""Test run(stream=True) method reuses an existing conversation ID from the session."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
session = AgentSession()
|
||||
session.service_session_id = "existing-conversation-id"
|
||||
|
||||
typing_activity = MagicMock()
|
||||
typing_activity.text = "Streaming response"
|
||||
typing_activity.type = "typing"
|
||||
typing_activity.id = "test-typing-id"
|
||||
typing_activity.from_property.name = "Test Bot"
|
||||
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([typing_activity])
|
||||
|
||||
response_count = 0
|
||||
async for response in agent.run("test message", session=session, stream=True):
|
||||
assert isinstance(response, AgentResponseUpdate)
|
||||
response_count += 1
|
||||
|
||||
assert response_count == 1
|
||||
assert session.service_session_id == "existing-conversation-id"
|
||||
mock_copilot_client.start_conversation.assert_not_called()
|
||||
mock_copilot_client.ask_question.assert_called_once_with("test message", "existing-conversation-id")
|
||||
|
||||
async def test_run_streaming_no_typing_activity(self, mock_copilot_client: MagicMock) -> None:
|
||||
"""Test run(stream=True) method with non-typing activity."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
message_activity = MagicMock()
|
||||
message_activity.text = "Message response"
|
||||
message_activity.type = "message"
|
||||
message_activity.id = "test-message-id"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([message_activity])
|
||||
|
||||
response_count = 0
|
||||
async for _response in agent.run("test message", stream=True):
|
||||
response_count += 1
|
||||
|
||||
assert response_count == 0
|
||||
|
||||
async def test_run_multiple_activities(self, mock_copilot_client: MagicMock) -> None:
|
||||
"""Test run method with multiple message activities."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
activity1 = MagicMock()
|
||||
activity1.text = "First response"
|
||||
activity1.type = "message"
|
||||
activity1.id = "test-id-1"
|
||||
activity1.from_property.name = "Test Bot"
|
||||
|
||||
activity2 = MagicMock()
|
||||
activity2.text = "Second response"
|
||||
activity2.type = "message"
|
||||
activity2.id = "test-id-2"
|
||||
activity2.from_property.name = "Test Bot"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([activity1, activity2])
|
||||
|
||||
response = await agent.run("test message")
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert len(response.messages) == 2
|
||||
|
||||
async def test_run_list_of_messages(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None:
|
||||
"""Test run method with list of messages."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
conversation_activity = MagicMock()
|
||||
conversation_activity.conversation.id = "test-conversation-id"
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity])
|
||||
|
||||
messages = ["Hello", "How are you?"]
|
||||
response = await agent.run(messages)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert len(response.messages) == 1
|
||||
|
||||
async def test_run_streaming_start_conversation_failure(self, mock_copilot_client: MagicMock) -> None:
|
||||
"""Test run(stream=True) method when conversation start fails."""
|
||||
agent = CopilotStudioAgent(client=mock_copilot_client)
|
||||
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([])
|
||||
|
||||
with pytest.raises(AgentException, match="Failed to start a new conversation"):
|
||||
async for _ in agent.run("test message", stream=True):
|
||||
pass
|
||||
Reference in New Issue
Block a user