chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,359 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from collections.abc import AsyncIterator
from types import SimpleNamespace
from typing import TypeVar
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pydantic import SecretStr, ValidationError
try:
import microsoft_agents.copilotstudio.client # type: ignore # noqa: F401
copilot_installed = True
except ImportError:
copilot_installed = False
pytestmark = pytest.mark.skipif(not copilot_installed, reason="`copilotstudio.client` is not installed")
if copilot_installed:
import semantic_kernel.agents.copilot_studio.copilot_studio_agent as csa_mod
from semantic_kernel.agents import (
CopilotStudioAgent,
CopilotStudioAgentSettings,
CopilotStudioAgentThread,
)
from semantic_kernel.agents.copilot_studio.copilot_studio_agent import (
_CopilotStudioAgentTokenFactory,
)
from semantic_kernel.agents.copilot_studio.copilot_studio_agent_settings import (
CopilotStudioAgentAuthMode,
)
from semantic_kernel.contents import AuthorRole, ChatMessageContent
from semantic_kernel.exceptions import (
AgentInitializationException,
AgentThreadInitializationException,
)
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
from semantic_kernel.prompt_template import PromptTemplateConfig
T = TypeVar("T")
@pytest.fixture
def aiter():
async def _aiter(items: list[T]) -> AsyncIterator[T]:
for item in items:
yield item
return _aiter
@pytest.fixture
def DummyConversation():
class DummyConversation:
def __init__(self, cid: str):
self.id = cid
return DummyConversation
@pytest.fixture
def DummyActivity(DummyConversation):
class DummyActivity:
def __init__(self, text: str, cid: str = "conv-123"):
self.type = "message"
self.text_format = "plain"
self.text = text
self.suggested_actions = None
self.conversation = DummyConversation(cid)
return DummyActivity
def test_initialize_thread_with_no_client_throws():
with pytest.raises(AgentThreadInitializationException, match="CopilotClient cannot be None"):
CopilotStudioAgentThread(client=None)
def test_normalize_messages():
client = MagicMock(spec=csa_mod.CopilotClient)
agent = CopilotStudioAgent(client=client)
single_str = "hello"
single_chat = ChatMessageContent(role=AuthorRole.USER, content="hola")
mixed = [single_str, single_chat]
assert agent._normalize_messages(None) == []
assert agent._normalize_messages(single_str) == ["hello"]
assert agent._normalize_messages(single_chat) == ["hola"]
assert agent._normalize_messages(mixed) == ["hello", "hola"]
def test_plugin_warning_emitted_once(caplog):
caplog.set_level(logging.WARNING)
dummy_kernel = Kernel()
dummy_kernel.add_plugin(SimpleNamespace(name="MyPlugin"))
agent = CopilotStudioAgent(client=MagicMock(spec=csa_mod.CopilotClient), kernel=dummy_kernel)
warn = [rec for rec in caplog.records if rec.levelno == logging.WARNING]
assert len(warn) == 1
assert "plugins will be ignored" in warn[0].getMessage()
caplog.clear()
_ = agent.model_copy()
assert not caplog.records
async def test_inner_invoke_prompt_and_yield(monkeypatch, aiter, DummyActivity):
client = MagicMock(spec=csa_mod.CopilotClient)
prompts: dict[str, str] = {}
async def fake_ask_question(*, question: str, conversation_id: str):
prompts["sent"] = question
yield DummyActivity("Aye matey!")
client.ask_question.side_effect = fake_ask_question
client.start_conversation = lambda: aiter([DummyActivity("", "conv-123")])
monkeypatch.setattr(
CopilotStudioAgent,
"format_instructions",
AsyncMock(return_value="Respond like a pirate"),
)
agent = CopilotStudioAgent(client=client)
thread = CopilotStudioAgentThread(client=client)
replies = [
msg
async for msg in agent._inner_invoke(
thread=thread, messages=["Tell me a joke about bears", "make the joke kid friendly"]
)
]
expected_prompt = "Respond like a pirate\nTell me a joke about bears\nmake the joke kid friendly"
assert prompts["sent"] == expected_prompt
assert len(replies) == 1
assert replies[0].content == "Aye matey!"
assert replies[0].role is AuthorRole.ASSISTANT
async def test_get_response(monkeypatch, aiter, DummyActivity):
client = MagicMock(spec=csa_mod.CopilotClient)
sent: dict[str, str] = {}
async def fake_ask_question(*, question: str, conversation_id: str):
sent["cid"] = conversation_id
yield DummyActivity("first", conversation_id)
yield DummyActivity("second", conversation_id)
client.ask_question.side_effect = fake_ask_question
client.start_conversation = lambda: aiter([DummyActivity("", "conv-123")])
monkeypatch.setattr(CopilotStudioAgent, "format_instructions", AsyncMock(return_value=None))
agent = CopilotStudioAgent(client=client)
thread = CopilotStudioAgentThread(client=client)
item = await agent.get_response(messages="hi there", thread=thread)
assert item.message.content == "second"
assert thread.id == "conv-123"
assert sent["cid"] == "conv-123"
assert item.thread is thread
async def test_invoke(monkeypatch, aiter, DummyActivity):
client = MagicMock(spec=csa_mod.CopilotClient)
sent: dict[str, str] = {}
async def fake_ask_question(*, question: str, conversation_id: str):
sent["cid"] = conversation_id
yield DummyActivity("first", conversation_id)
yield DummyActivity("second", conversation_id)
client.ask_question.side_effect = fake_ask_question
client.start_conversation = lambda: aiter([DummyActivity("", "conv-123")])
monkeypatch.setattr(CopilotStudioAgent, "format_instructions", AsyncMock(return_value=None))
agent = CopilotStudioAgent(
client=client,
prompt_template_config=PromptTemplateConfig(template="Handle the message in this {{$style}}"),
)
thread = CopilotStudioAgentThread(client=client)
responses = []
async for response in agent.invoke(
messages="hi there", thread=thread, arguments=KernelArguments(style="funny")
):
responses.append(response)
item = responses[-1]
assert item.message.content == "second"
assert thread.id == "conv-123"
assert sent["cid"] == "conv-123"
assert item.thread is thread
def test_setup_resources_settings_validation_error():
sentinel_exc = ValidationError.from_exception_data("dummy", [], input_type="python")
with (
patch(
"semantic_kernel.agents.copilot_studio.copilot_studio_agent.CopilotStudioAgentSettings",
side_effect=sentinel_exc,
),
pytest.raises(AgentInitializationException, match="Failed to create Copilot Studio Agent settings"),
):
_ = CopilotStudioAgent.create_client(app_client_id="appid", tenant_id="tenantid")
def test_setup_resources_missing_ids():
dummy_settings = MagicMock(spec=CopilotStudioAgentSettings)
dummy_settings.app_client_id = None
dummy_settings.tenant_id = None
with (
patch(
"semantic_kernel.agents.copilot_studio.copilot_studio_agent.CopilotStudioAgentSettings",
return_value=dummy_settings,
),
pytest.raises(
AgentInitializationException,
match="Missing required configuration field\\(s\\): app_client_id, tenant_id",
),
):
_ = CopilotStudioAgent.create_client()
def test_setup_resources_happy_path(tmp_path, monkeypatch):
dummy_settings = MagicMock(spec=CopilotStudioAgentSettings)
dummy_settings.app_client_id = "appid"
dummy_settings.tenant_id = "tenantid"
dummy_settings.auth_mode = CopilotStudioAgentAuthMode.INTERACTIVE
dummy_settings.client_secret = SecretStr("test-secret")
monkeypatch.setattr(
"semantic_kernel.agents.copilot_studio.copilot_studio_agent.CopilotStudioAgentSettings",
lambda **_: dummy_settings,
)
monkeypatch.setattr(_CopilotStudioAgentTokenFactory, "acquire", lambda self: "fake-token")
sentinel_client = MagicMock(spec=csa_mod.CopilotClient)
with patch(
"semantic_kernel.agents.copilot_studio.copilot_studio_agent.CopilotClient", return_value=sentinel_client
) as mock_client_ctor:
cache_path = tmp_path / "cache.bin"
monkeypatch.setenv("TOKEN_CACHE_PATH", str(cache_path))
returned = CopilotStudioAgent.create_client(
app_client_id="appid",
tenant_id="tenantid",
environment_id="env-id",
agent_identifier="agent-name",
auth_mode=CopilotStudioAgentAuthMode.SERVICE,
)
mock_client_ctor.assert_called_once_with(dummy_settings, "fake-token")
assert returned is sentinel_client
class DummyCache:
pass
class FakeAppSilent:
def __init__(self, client_id, authority, token_cache, client_credential=None, **kwargs):
pass
def get_accounts(self):
return [{"home_account_id": "acct1"}]
def acquire_token_silent(self, scopes, account):
return {"access_token": "silent-token"}
def acquire_token_interactive(self, scopes):
pytest.skip("Unexpected interactive flow in silent test")
class FakeAppInteractive:
def __init__(self, client_id, authority, token_cache):
pass
def get_accounts(self):
return []
def acquire_token_silent(self, scopes, account):
pytest.skip("Unexpected silent flow in interactive test")
def acquire_token_interactive(self, scopes):
return {"access_token": "interactive-token"}
class FakeAppError:
def __init__(self, client_id, authority, token_cache):
pass
def get_accounts(self):
return []
def acquire_token_silent(self, scopes, account):
return {}
def acquire_token_interactive(self, scopes):
return {
"error": "bad",
"error_description": "failed",
"correlation_id": "cid",
}
@pytest.fixture(autouse=True)
def stub_cache(monkeypatch):
monkeypatch.setattr(
_CopilotStudioAgentTokenFactory,
"_get_msal_token_cache",
staticmethod(lambda cache_path, fallback_to_plaintext=True: DummyCache()),
)
@pytest.mark.parametrize(
"fake_app, expected_token, mode",
[
pytest.param(
FakeAppSilent,
"silent-token",
CopilotStudioAgentAuthMode.SERVICE,
marks=pytest.mark.skip(reason="Skipping SERVICE auth mode test as the mode is not yet supported."),
),
(FakeAppInteractive, "interactive-token", CopilotStudioAgentAuthMode.INTERACTIVE),
],
)
def test_acquire_token_success(monkeypatch, tmp_path, fake_app, expected_token, mode):
settings = CopilotStudioAgentSettings(app_client_id="id", tenant_id="tid")
cache_path = str(tmp_path / "cache.bin")
monkeypatch.setattr(csa_mod, "PublicClientApplication", fake_app)
monkeypatch.setattr(csa_mod, "ConfidentialClientApplication", fake_app)
client_secret = None
if fake_app == FakeAppSilent:
client_secret = "test-secret"
factory = _CopilotStudioAgentTokenFactory(
settings=settings,
cache_path=cache_path,
mode=mode,
client_secret=client_secret,
client_certificate=None,
user_assertion=None,
)
token = factory.acquire()
assert token == expected_token
def test_acquire_token_error(monkeypatch, tmp_path):
settings = CopilotStudioAgentSettings(app_client_id="id", tenant_id="tid")
cache_path = str(tmp_path / "cache.bin")
monkeypatch.setattr(csa_mod, "PublicClientApplication", FakeAppError)
monkeypatch.setattr(csa_mod, "ConfidentialClientApplication", FakeAppError)
factory = _CopilotStudioAgentTokenFactory(
settings=settings,
cache_path=cache_path,
mode=CopilotStudioAgentAuthMode.INTERACTIVE,
client_secret=None,
client_certificate=None,
user_assertion=None,
)
with pytest.raises(AgentInitializationException):
factory.acquire()