From 109199946bd9e1230f5405106ccbdc4feac975cb Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 11 Jun 2026 15:12:33 -0700 Subject: [PATCH 1/9] agt init foundations --- .../testing/cli/commands/__init__.py | 2 + .../testing/cli/commands/init.py | 98 +++++++++++++++++++ .../presets/basic/e2e-tests/.gitignore | 3 + .../presets/basic/e2e-tests/config.json | 9 ++ .../presets/basic/e2e-tests/env.TEMPLATE | 0 .../presets/basic/e2e-tests/pyproject.toml | 9 ++ .../presets/basic/e2e-tests/pytest.ini | 30 ++++++ .../presets/basic/e2e-tests/tests/__init__.py | 3 + .../basic/e2e-tests/tests/test_my_agent.py | 11 +++ .../presets/basic/my_agent/env.TEMPLATE | 0 .../presets/basic/my_agent/pyproject.toml | 0 .../presets/basic/my_agent/src/__init__.py | 0 .../presets/basic/my_agent/src/main.py | 61 ++++++++++++ .../basic/my_agent/src/start_server.py | 33 +++++++ .../presets/localhost/e2e-tests/.gitignore | 3 + .../presets/localhost/e2e-tests/config.json | 7 ++ .../presets/localhost/e2e-tests/env.TEMPLATE | 0 .../localhost/e2e-tests/pyproject.toml | 9 ++ .../presets/localhost/e2e-tests/pytest.ini | 30 ++++++ .../localhost/e2e-tests/tests/__init__.py | 3 + .../e2e-tests/tests/test_my_agent.py | 11 +++ .../testing/scenario_registry.py | 37 +++++++ .../microsoft-agents-testing/pyproject.toml | 5 +- 23 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/__init__.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py index 31bf1c6a6..6dadd2337 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py @@ -12,11 +12,13 @@ # Import commands from .environment import env_group from .scenario import scenario_group +from .init import init_group # Add commands to this list to register them with the CLI COMMANDS: list[Command] = [ env_group, scenario_group, + init_group, ] __all__ = ["COMMANDS"] \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py new file mode 100644 index 000000000..afc186f49 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py @@ -0,0 +1,98 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Init CLI command — scaffold a test harness from a preset template.""" + +import shutil +from pathlib import Path +from importlib.resources import files as _pkg_files + +import click + +from microsoft_agents.testing.cli.core import pass_output, Output + +# --------------------------------------------------------------------------- +# Preset discovery — runs once at import time +# --------------------------------------------------------------------------- +_PRESETS_ROOT = _pkg_files("microsoft_agents.testing") / "presets" + + +def _discover_presets() -> dict: + try: + return { + entry.name: entry + for entry in _PRESETS_ROOT.iterdir() + if entry.is_dir() + } + except Exception: + return {} + + +PRESETS: dict = _discover_presets() + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def _copy_traversable(src, dest: Path) -> None: + """Recursively copy a Traversable tree into an existing dest directory.""" + for item in src.iterdir(): + target = dest / item.name + if item.is_dir(): + target.mkdir(parents=True, exist_ok=True) + _copy_traversable(item, target) + else: + target.write_bytes(item.read_bytes()) + + +# --------------------------------------------------------------------------- +# Command +# --------------------------------------------------------------------------- +@click.command(name="init") +@click.argument("preset", required=False, default=None) +@click.option("--force", is_flag=True, default=False, help="Overwrite any existing files from the preset.") +@pass_output +def init_group(out: Output, preset: str, force: bool) -> None: + """Scaffold a test harness from a preset template. + + PRESET is the name of the harness template to use. + Omit PRESET to list available presets. + """ + if not preset: + if not PRESETS: + out.warning("No presets found.") + else: + out.info("Available presets:") + for name in PRESETS: + out.info(f"\t{name}") + return + + if preset not in PRESETS: + available = ", ".join(PRESETS) or "none" + out.error(f"Unknown preset '{preset}'. Available: {available}") + raise SystemExit(1) + + cwd = Path.cwd() + preset_traversable = PRESETS[preset] + + conflicts = [ + cwd / item.name + for item in preset_traversable.iterdir() + if (cwd / item.name).exists() + ] + + if conflicts and not force: + names = ", ".join(p.name for p in conflicts) + out.error(f"Already exists: {names}. Use --force to overwrite.") + raise SystemExit(1) + + if force: + for conflict in conflicts: + if conflict.is_dir(): + shutil.rmtree(conflict) + else: + conflict.unlink() + + out.info(f"Initializing preset '{preset}' ...") + _copy_traversable(preset_traversable, cwd) + out.success(f"Done. Files written to {cwd}") diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore new file mode 100644 index 000000000..d6deddae7 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore @@ -0,0 +1,3 @@ +*.env +appsettings.* +appsettings.*.json \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json new file mode 100644 index 000000000..9c22796d0 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json @@ -0,0 +1,9 @@ +{ + "agents": { + "my_agent": { + "path": "../my_agent", + "setup": "uv sync", + "run": "uv run python -m src.main" + } + } +} \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE new file mode 100644 index 000000000..e69de29bb diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml new file mode 100644 index 000000000..a3220cdaf --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "e2e-tests" +version = "0.0.0" +requires-python = ">=3.13" +dependencies = [ + "pytest", + "pytest-asyncio", + "microsoft-agents-testing @ git+https://github.com/microsoft/Agents-for-python.git@main#subdirectory=dev/testing/microsoft-agents-testing", +] \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini new file mode 100644 index 000000000..58491634c --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini @@ -0,0 +1,30 @@ +[pytest] +# pytest config for the environments/local test suite. +# Run locally: uv run pytest tests/ +# Or via Docker: ./scripts/run_local.ps1 + +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning + ignore::aiohttp.web.NotAppKeyWarning + +asyncio_mode = auto + +addopts = + --strict-markers + --strict-config + --verbose + --tb=short + --durations=10 + +minversion = 6.0 + +markers = + unit: Unit tests + integration: Integration tests + slow: Slow tests that may take longer to run + requires_network: Tests that require network access + requires_auth: Tests that require authentication + +asyncio_default_fixture_loop_scope = class +asyncio_default_test_loop_scope = class \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py new file mode 100644 index 000000000..11703591e --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py @@ -0,0 +1,3 @@ +from microsoft_agents.testing import scenario_registry + +scenario_registry.load_json("config.json") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py new file mode 100644 index 000000000..14d56fd35 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py @@ -0,0 +1,11 @@ +import pytest + +from microsoft_agents.testing import AgentClient + +@pytest.mark.agent_test("my_agent") +async def test_my_agent(agent_client: AgentClient): + + await agent_client.send("Hello, World!", wait=5.0) + + # assert that the agent replied with a message Activity + agent_client.expect().that_for_any(type="message") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE new file mode 100644 index 000000000..e69de29bb diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml new file mode 100644 index 000000000..e69de29bb diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py new file mode 100644 index 000000000..408ffd2c0 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging +from os import environ, path +from dotenv import load_dotenv +from microsoft_agents.activity import load_configuration_from_env +from microsoft_agents.authentication.msal import ( + MsalConnectionManager, +) +from microsoft_agents.hosting.core import ( + AgentApplication, + TurnState, + TurnContext, + MemoryStorage, +) +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core.app.oauth.authorization import Authorization + +from .start_server import start_server + +logging.basicConfig(level=logging.INFO) +load_dotenv(path.join(path.dirname(__file__), ".env")) + +agents_sdk_config = load_configuration_from_env(environ) + +STORAGE = MemoryStorage() +CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config) +ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER) +AUTHORIZATION = Authorization(STORAGE, CONNECTION_MANAGER, **agents_sdk_config) + +AGENT_APP = AgentApplication[TurnState]( + storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config +) + + +async def _help(context: TurnContext, _state: TurnState): + await context.send_activity( + "Welcome to the Echo Agent sample 🚀. " + "Type /help for help or send a message to see the echo feature in action." + ) + + +AGENT_APP.conversation_update("membersAdded")(_help) + +AGENT_APP.message("/help")(_help) + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, _): + await context.send_activity(f"you said: {context.activity.text}") + + +if __name__ == "__main__": + try: + start_server( + agent_application=AGENT_APP, + auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), + ) + except Exception as error: + raise error diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py new file mode 100644 index 000000000..fd1846a6d --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py @@ -0,0 +1,33 @@ +from os import environ +from microsoft_agents.hosting.core import AgentApplication, AgentAuthConfiguration +from microsoft_agents.hosting.aiohttp import ( + start_agent_process, + jwt_authorization_middleware, + CloudAdapter, +) +from aiohttp.web import Request, Response, Application, run_app + + +def start_server( + agent_application: AgentApplication, auth_configuration: AgentAuthConfiguration +): + async def entry_point(req: Request) -> Response: + agent: AgentApplication = req.app["agent_app"] + adapter: CloudAdapter = req.app["adapter"] + return await start_agent_process( + req, + agent, + adapter, + ) + + APP = Application(middlewares=[jwt_authorization_middleware]) + APP.router.add_post("/api/messages", entry_point) + APP.router.add_get("/api/messages", lambda _: Response(status=200)) + APP["agent_configuration"] = auth_configuration + APP["agent_app"] = agent_application + APP["adapter"] = agent_application.adapter + + try: + run_app(APP, host="localhost", port=environ.get("PORT", 3978)) + except Exception as error: + raise error diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore new file mode 100644 index 000000000..d6deddae7 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore @@ -0,0 +1,3 @@ +*.env +appsettings.* +appsettings.*.json \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json new file mode 100644 index 000000000..f4cc7bc59 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json @@ -0,0 +1,7 @@ +{ + "agents": { + "my_agent": { + "path": "http://localhost:3978/api/messages" + } + } +} \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE new file mode 100644 index 000000000..e69de29bb diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml new file mode 100644 index 000000000..a3220cdaf --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "e2e-tests" +version = "0.0.0" +requires-python = ">=3.13" +dependencies = [ + "pytest", + "pytest-asyncio", + "microsoft-agents-testing @ git+https://github.com/microsoft/Agents-for-python.git@main#subdirectory=dev/testing/microsoft-agents-testing", +] \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini new file mode 100644 index 000000000..58491634c --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini @@ -0,0 +1,30 @@ +[pytest] +# pytest config for the environments/local test suite. +# Run locally: uv run pytest tests/ +# Or via Docker: ./scripts/run_local.ps1 + +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning + ignore::aiohttp.web.NotAppKeyWarning + +asyncio_mode = auto + +addopts = + --strict-markers + --strict-config + --verbose + --tb=short + --durations=10 + +minversion = 6.0 + +markers = + unit: Unit tests + integration: Integration tests + slow: Slow tests that may take longer to run + requires_network: Tests that require network access + requires_auth: Tests that require authentication + +asyncio_default_fixture_loop_scope = class +asyncio_default_test_loop_scope = class \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py new file mode 100644 index 000000000..11703591e --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py @@ -0,0 +1,3 @@ +from microsoft_agents.testing import scenario_registry + +scenario_registry.load_json("config.json") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py new file mode 100644 index 000000000..14d56fd35 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py @@ -0,0 +1,11 @@ +import pytest + +from microsoft_agents.testing import AgentClient + +@pytest.mark.agent_test("my_agent") +async def test_my_agent(agent_client: AgentClient): + + await agent_client.send("Hello, World!", wait=5.0) + + # assert that the agent replied with a message Activity + agent_client.expect().that_for_any(type="message") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py index 618c8f341..8933d33de 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py @@ -27,6 +27,7 @@ import sys import importlib +import json from pathlib import Path from fnmatch import fnmatch from dataclasses import dataclass @@ -107,6 +108,42 @@ def register( description=description, ) + def load_json(self, file_path: str) -> None: + """Load scenarios from a JSON file.""" + + with open(file_path, "r") as f: + data = json.load(f) + + agent_defs = data.get("agents", {}) + + for name, body in agent_defs.items(): + + path_str = body.get("path", "") + desc = body.get("description", "") + + if "http" in path_str: + self.register( + name, + ExternalScenario(path_str), + description=desc, + ) + else: + path = Path(path_str) + if not path.exists(): + raise FileNotFoundError(f"Scenario file not found: {path}") + self.register( + name, + SourceScenario(path), + description=desc, + ) + + for name, scenario_data in data.items(): + self.register( + name, + ExternalScenario(url=scenario_data["url"]), + description=scenario_data.get("description", ""), + ) + def get_entry(self, name: str) -> ScenarioEntry: """Get the full entry (scenario + metadata) by name.""" if name not in self._entries: diff --git a/dev/testing/microsoft-agents-testing/pyproject.toml b/dev/testing/microsoft-agents-testing/pyproject.toml index c08c4a851..92f97a856 100644 --- a/dev/testing/microsoft-agents-testing/pyproject.toml +++ b/dev/testing/microsoft-agents-testing/pyproject.toml @@ -44,4 +44,7 @@ dependencies = [ agt = "microsoft_agents.testing.cli:main" [project.entry-points.pytest11] -agent_test = "microsoft_agents.testing.pytest_plugin" \ No newline at end of file +agent_test = "microsoft_agents.testing.pytest_plugin" + +[tool.setuptools.package-data] +"microsoft_agents.testing" = ["presets/**/*"] \ No newline at end of file From a1fdf116478879d78ca2a02c7a79e33480909089 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 15 Jun 2026 14:00:30 -0700 Subject: [PATCH 2/9] SourceScenario improvements --- .../testing/cross_sdk/source_scenario.py | 14 ++--- .../presets/basic/my_agent/pyproject.toml | 12 ++++ .../presets/basic/my_agent/src/main.py | 2 +- .../presets/localhost/e2e-tests/.gitignore | 3 - .../presets/localhost/e2e-tests/config.json | 7 --- .../presets/localhost/e2e-tests/env.TEMPLATE | 0 .../localhost/e2e-tests/pyproject.toml | 9 --- .../presets/localhost/e2e-tests/pytest.ini | 30 --------- .../localhost/e2e-tests/tests/__init__.py | 3 - .../e2e-tests/tests/test_my_agent.py | 11 ---- .../testing/scenario_registry.py | 18 +++--- .../microsoft-agents-testing/pyproject.toml | 1 - .../tests/cross_sdk/test_source_scenario.py | 62 +++++++------------ 13 files changed, 50 insertions(+), 122 deletions(-) delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/source_scenario.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/source_scenario.py index 7e74f8c0a..16ba7d604 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/source_scenario.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/source_scenario.py @@ -4,7 +4,6 @@ import asyncio import shutil import subprocess -import sys from pathlib import Path @@ -70,29 +69,30 @@ class SourceScenario(ExternalScenario): def __init__( self, - script_path: str, + agent_path: str | Path, + script: str, delay: float = 0.0, config: ScenarioConfig | None = None ) -> None: super().__init__(DEFAULT_LOCAL_AGENT_ENDPOINT, config) - self._script_path = Path(script_path) + self._agent_path = Path(agent_path) + self._script = script self._delay = delay @asynccontextmanager async def _run_script(self) -> AsyncIterator[None]: - script_path = self._script_path.resolve() + agent_path = self._agent_path.resolve() runner = shutil.which("pwsh") or shutil.which("powershell") if runner is None: raise FileNotFoundError("Could not find pwsh or powershell in PATH") process = subprocess.Popen( - [runner, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script_path)], + [runner, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", self._script], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=script_path.parent, - # shell=sys.platform == "win32", + cwd=agent_path, ) try: diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml index e69de29bb..3caefca29 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "my-agent" +version = "0.0.0" +requires-python = ">=3.13" +dependencies = [ + "python-dotenv", + "aiohttp", + "microsoft-agents-hosting-aiohttp", + "microsoft-agents-hosting-core", + "microsoft-agents-authentication-msal", + "microsoft-agents-activity" +] \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py index 408ffd2c0..e9c4bc75e 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py @@ -20,7 +20,7 @@ from .start_server import start_server logging.basicConfig(level=logging.INFO) -load_dotenv(path.join(path.dirname(__file__), ".env")) +load_dotenv() agents_sdk_config = load_configuration_from_env(environ) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore deleted file mode 100644 index d6deddae7..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.env -appsettings.* -appsettings.*.json \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json deleted file mode 100644 index f4cc7bc59..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "agents": { - "my_agent": { - "path": "http://localhost:3978/api/messages" - } - } -} \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml deleted file mode 100644 index a3220cdaf..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml +++ /dev/null @@ -1,9 +0,0 @@ -[project] -name = "e2e-tests" -version = "0.0.0" -requires-python = ">=3.13" -dependencies = [ - "pytest", - "pytest-asyncio", - "microsoft-agents-testing @ git+https://github.com/microsoft/Agents-for-python.git@main#subdirectory=dev/testing/microsoft-agents-testing", -] \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini deleted file mode 100644 index 58491634c..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini +++ /dev/null @@ -1,30 +0,0 @@ -[pytest] -# pytest config for the environments/local test suite. -# Run locally: uv run pytest tests/ -# Or via Docker: ./scripts/run_local.ps1 - -filterwarnings = - ignore::DeprecationWarning - ignore::PendingDeprecationWarning - ignore::aiohttp.web.NotAppKeyWarning - -asyncio_mode = auto - -addopts = - --strict-markers - --strict-config - --verbose - --tb=short - --durations=10 - -minversion = 6.0 - -markers = - unit: Unit tests - integration: Integration tests - slow: Slow tests that may take longer to run - requires_network: Tests that require network access - requires_auth: Tests that require authentication - -asyncio_default_fixture_loop_scope = class -asyncio_default_test_loop_scope = class \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py deleted file mode 100644 index 11703591e..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from microsoft_agents.testing import scenario_registry - -scenario_registry.load_json("config.json") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py deleted file mode 100644 index 14d56fd35..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - -from microsoft_agents.testing import AgentClient - -@pytest.mark.agent_test("my_agent") -async def test_my_agent(agent_client: AgentClient): - - await agent_client.send("Hello, World!", wait=5.0) - - # assert that the agent replied with a message Activity - agent_client.expect().that_for_any(type="message") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py index 8933d33de..2663f43d4 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py @@ -34,6 +34,7 @@ from collections.abc import Iterator from .core import Scenario, ExternalScenario +from .cross_sdk import SourceScenario @dataclass(frozen=True) class ScenarioEntry: @@ -120,6 +121,7 @@ def load_json(self, file_path: str) -> None: path_str = body.get("path", "") desc = body.get("description", "") + script = body.get("script", "") if "http" in path_str: self.register( @@ -128,22 +130,19 @@ def load_json(self, file_path: str) -> None: description=desc, ) else: + + if not script: + raise ValueError("A 'script' field is required for source scenarios") + path = Path(path_str) if not path.exists(): raise FileNotFoundError(f"Scenario file not found: {path}") self.register( name, - SourceScenario(path), + SourceScenario(path, script), description=desc, ) - for name, scenario_data in data.items(): - self.register( - name, - ExternalScenario(url=scenario_data["url"]), - description=scenario_data.get("description", ""), - ) - def get_entry(self, name: str) -> ScenarioEntry: """Get the full entry (scenario + metadata) by name.""" if name not in self._entries: @@ -213,7 +212,8 @@ def clear(self) -> None: """Remove all registered scenarios. Primarily for testing.""" self._entries.clear() -# Global singleton instance +# Global singlet +# on instance scenario_registry = ScenarioRegistry() diff --git a/dev/testing/microsoft-agents-testing/pyproject.toml b/dev/testing/microsoft-agents-testing/pyproject.toml index 92f97a856..31fe8fb36 100644 --- a/dev/testing/microsoft-agents-testing/pyproject.toml +++ b/dev/testing/microsoft-agents-testing/pyproject.toml @@ -9,7 +9,6 @@ description = "Core library for Microsoft Agents" readme = {file = "README.md", content-type = "text/markdown"} authors = [{name = "Microsoft Corporation"}] license = "MIT" -license-files = ["LICENSE"] requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", diff --git a/dev/testing/microsoft-agents-testing/tests/cross_sdk/test_source_scenario.py b/dev/testing/microsoft-agents-testing/tests/cross_sdk/test_source_scenario.py index d6e979c38..de2889ff5 100644 --- a/dev/testing/microsoft-agents-testing/tests/cross_sdk/test_source_scenario.py +++ b/dev/testing/microsoft-agents-testing/tests/cross_sdk/test_source_scenario.py @@ -15,26 +15,25 @@ class TestSourceScenarioInit: - def test_stores_script_path(self, tmp_path): - script = tmp_path / "run_agent.ps1" - scenario = SourceScenario(str(script)) - assert scenario._script_path == pathlib.Path(str(script)) + def test_stores_agent_path_and_script(self, tmp_path): + scenario = SourceScenario(str(tmp_path), "python agent.py") + assert scenario._agent_path == pathlib.Path(str(tmp_path)) + assert scenario._script == "python agent.py" def test_default_delay_is_zero(self, tmp_path): - scenario = SourceScenario(str(tmp_path / "run_agent.ps1")) + scenario = SourceScenario(str(tmp_path), "python agent.py") assert scenario._delay == 0.0 def test_custom_delay(self, tmp_path): - scenario = SourceScenario(str(tmp_path / "run_agent.ps1"), delay=15.0) + scenario = SourceScenario(str(tmp_path), "python agent.py", delay=15.0) assert scenario._delay == 15.0 def test_uses_default_endpoint(self, tmp_path): - scenario = SourceScenario(str(tmp_path / "run_agent.ps1")) - # ExternalScenario stores the endpoint; access via the attribute set by super().__init__ + scenario = SourceScenario(str(tmp_path), "python agent.py") assert scenario._endpoint == constants.DEFAULT_LOCAL_AGENT_ENDPOINT def test_accepts_none_config(self, tmp_path): - scenario = SourceScenario(str(tmp_path / "run_agent.ps1"), config=None) + scenario = SourceScenario(str(tmp_path), "python agent.py", config=None) assert scenario is not None @@ -47,9 +46,7 @@ def _stub_process() -> MagicMock: class TestRunScriptPwshDetection: async def test_raises_when_no_powershell_found(self, tmp_path): - script = tmp_path / "run_agent.ps1" - script.write_text("# stub") - scenario = SourceScenario(str(script)) + scenario = SourceScenario(str(tmp_path), "python agent.py") with patch("shutil.which", return_value=None): with pytest.raises(FileNotFoundError, match="pwsh"): @@ -57,9 +54,7 @@ async def test_raises_when_no_powershell_found(self, tmp_path): pass async def test_prefers_pwsh_over_powershell(self, tmp_path): - script = tmp_path / "run_agent.ps1" - script.write_text("# stub") - scenario = SourceScenario(str(script)) + scenario = SourceScenario(str(tmp_path), "python agent.py") captured = {} @@ -85,9 +80,7 @@ def _fake_which(name): assert captured["cmd"][0] == "/usr/bin/pwsh" async def test_falls_back_to_powershell(self, tmp_path): - script = tmp_path / "run_agent.ps1" - script.write_text("# stub") - scenario = SourceScenario(str(script)) + scenario = SourceScenario(str(tmp_path), "python agent.py") captured = {} @@ -112,12 +105,10 @@ def _fake_which(name): assert captured["cmd"][0] == "C:\\Windows\\powershell.exe" - async def test_script_cwd_is_parent_directory(self, tmp_path): + async def test_script_cwd_is_agent_path(self, tmp_path): agent_dir = tmp_path / "my_agent" agent_dir.mkdir() - script = agent_dir / "run_agent.ps1" - script.write_text("# stub") - scenario = SourceScenario(str(script)) + scenario = SourceScenario(str(agent_dir), "python agent.py") mock_process = _stub_process() @@ -134,7 +125,7 @@ async def test_script_cwd_is_parent_directory(self, tmp_path): pass _, kwargs = mock_popen.call_args - assert kwargs["cwd"] == script.resolve().parent + assert kwargs["cwd"] == agent_dir.resolve() @pytest.mark.slow @@ -146,10 +137,6 @@ async def test_two_scenarios_sequentially_stop_on_context_exit(self, tmp_path): if not (shutil.which("pwsh") or shutil.which("powershell")): pytest.skip("pwsh/powershell not available on PATH") - # Script never exits on its own — the only way it stops is via terminate(). - script = tmp_path / "run_forever.ps1" - script.write_text("while ($true) { Start-Sleep -Seconds 1 }\n") - captured: list[subprocess.Popen] = [] real_popen = subprocess.Popen @@ -163,21 +150,20 @@ def _capture_popen(*args, **kwargs): side_effect=_capture_popen, ): for run_index in range(2): - scenario = SourceScenario(str(script)) + scenario = SourceScenario( + str(tmp_path), + "while ($true) { Start-Sleep -Seconds 1 }", + ) async with scenario._run_script(): - # Process must be alive inside the context. assert captured[-1].poll() is None, ( f"Scenario {run_index} exited before the context began" ) await asyncio.sleep(5) - # Still alive right before the context exits — proves "indefinitely". assert captured[-1].poll() is None, ( f"Scenario {run_index} exited on its own during the 5s window" ) - # External check: leaving the context must have shut the process down. - # Allow a brief grace window in case terminate/wait hasn't fully reaped yet. deadline = time.monotonic() + 2.0 while time.monotonic() < deadline and captured[-1].poll() is None: await asyncio.sleep(0.05) @@ -190,7 +176,7 @@ def _capture_popen(*args, **kwargs): async def test_child_process_is_killed_when_context_exits(self, tmp_path): """Reproducer for orphaned-child bug. - A real run_agent.ps1 launches a child (e.g. ``python agent.py``). + A real script launches a child (e.g. ``python agent.py``). terminate() on the pwsh PID does NOT kill its descendants on Windows or POSIX, so the child becomes an orphan. We detect this by watching a heartbeat file the child writes every 0.2s — if its mtime keeps @@ -208,9 +194,7 @@ async def test_child_process_is_killed_when_context_exits(self, tmp_path): " time.sleep(0.2)\n" ) - script = tmp_path / "run_agent.ps1" - # & invokes a native command in pwsh; quoted paths handle spaces. - script.write_text(f'& "{sys.executable}" "{child}"\n') + ps_command = f'& "{sys.executable}" "{child}"' captured: list[subprocess.Popen] = [] real_popen = subprocess.Popen @@ -225,17 +209,14 @@ def _capture_popen(*args, **kwargs): "microsoft_agents.testing.cross_sdk.source_scenario.subprocess.Popen", side_effect=_capture_popen, ): - scenario = SourceScenario(str(script)) + scenario = SourceScenario(str(tmp_path), ps_command) async with scenario._run_script(): - # Wait for the child to come up and start heartbeating. deadline = time.monotonic() + 5.0 while time.monotonic() < deadline and not heartbeat.exists(): await asyncio.sleep(0.1) assert heartbeat.exists(), "Child never started heartbeating" await asyncio.sleep(1.0) - # Let any in-flight terminate() complete, then snapshot mtime - # and watch for further updates. await asyncio.sleep(1.0) mtime_after_exit = heartbeat.stat().st_mtime await asyncio.sleep(2.0) @@ -247,7 +228,6 @@ def _capture_popen(*args, **kwargs): "wrapper but not its descendants." ) finally: - # Defensive cleanup so a failed test doesn't leak the orphan. if sys.platform == "win32": for proc in captured: subprocess.run( From 992238fa9cd99d4f9ac17caf6630a39671c807f7 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 15 Jun 2026 14:08:02 -0700 Subject: [PATCH 3/9] Adding extra post init information messages --- .../microsoft_agents/testing/cli/commands/init.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py index afc186f49..086e53d4c 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py @@ -96,3 +96,15 @@ def init_group(out: Output, preset: str, force: bool) -> None: out.info(f"Initializing preset '{preset}' ...") _copy_traversable(preset_traversable, cwd) out.success(f"Done. Files written to {cwd}") + + out.info("") + out.info("Next step: create .env files for each component from their env.TEMPLATE:") + env_templates = [ + cwd / item.name / "env.TEMPLATE" + for item in preset_traversable.iterdir() + if item.is_dir() and (cwd / item.name / "env.TEMPLATE").exists() + ] + for template in env_templates: + out.info(f"\t{template.parent.name}/env.TEMPLATE → {template.parent.name}/.env") + out.info("") + out.info("Populate each .env with your app credentials and configuration before running.") From 31bbd8069b9d845875fec8b83b06be6fd3a27472 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 15 Jun 2026 14:23:14 -0700 Subject: [PATCH 4/9] Fixing tests --- .../microsoft_agents/testing/__init__.py | 5 +++- .../testing/{cross_sdk => }/constants.py | 1 - .../testing/cross_sdk/__init__.py | 16 ---------- .../testing/cross_sdk/types.py | 10 ------- .../testing/cross_sdk/utils.py | 27 ----------------- .../presets/basic/e2e-tests/.gitignore | 3 -- .../presets/basic/e2e-tests/env.TEMPLATE | 3 ++ .../presets/basic/my_agent/env.TEMPLATE | 3 ++ .../presets/localhost/e2e-tests/config.json | 7 +++++ .../presets/localhost/e2e-tests/env.TEMPLATE | 3 ++ .../localhost/e2e-tests/pyproject.toml | 9 ++++++ .../presets/localhost/e2e-tests/pytest.ini | 30 +++++++++++++++++++ .../localhost/e2e-tests/tests/__init__.py | 3 ++ .../e2e-tests/tests/test_my_agent.py | 11 +++++++ .../testing/scenario_registry.py | 2 +- .../{cross_sdk => }/source_scenario.py | 15 ---------- .../tests/cross_sdk/__init__.py | 0 .../{cross_sdk => }/test_source_scenario.py | 22 +++++++------- 18 files changed, 84 insertions(+), 86 deletions(-) rename dev/testing/microsoft-agents-testing/microsoft_agents/testing/{cross_sdk => }/constants.py (82%) delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/__init__.py delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/types.py delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/utils.py delete mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py create mode 100644 dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py rename dev/testing/microsoft-agents-testing/microsoft_agents/testing/{cross_sdk => }/source_scenario.py (91%) delete mode 100644 dev/testing/microsoft-agents-testing/tests/cross_sdk/__init__.py rename dev/testing/microsoft-agents-testing/tests/{cross_sdk => }/test_source_scenario.py (91%) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/__init__.py index 052524a17..c01abad58 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/__init__.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/__init__.py @@ -52,6 +52,8 @@ Unset, ) +from .source_scenario import SourceScenario + from .aiohttp_scenario import ( AgentEnvironment, AiohttpScenario, @@ -107,5 +109,6 @@ "TranscriptFormatter", "print_activities", "print_conversation", - "print_json" + "print_json", + "SourceScenario" ] diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/constants.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/constants.py similarity index 82% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/constants.py rename to dev/testing/microsoft-agents-testing/microsoft_agents/testing/constants.py index 10cd01eac..c5065c997 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/constants.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/constants.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -ENTRY_POINT_NAME = "run_agent.ps1" DEFAULT_LOCAL_AGENT_ENDPOINT = "http://localhost:3978/api/messages" diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/__init__.py deleted file mode 100644 index d2507a34a..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from . import constants -from .types import SDKVersion - -from .utils import ( - create_agent_path, - create_scenario, -) -from .source_scenario import SourceScenario - -__all__ = [ - "constants", - "create_agent_path", - "create_scenario", - "SDKVersion", - "SourceScenario", -] \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/types.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/types.py deleted file mode 100644 index e3bc31e55..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/types.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from enum import Enum - -class SDKVersion(str, Enum): - - PYTHON = "python" - NODEJS = "nodejs" - DOTNET = "dotnet" \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/utils.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/utils.py deleted file mode 100644 index b5b0d82da..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from pathlib import Path - -from microsoft_agents.testing import Scenario - -from . import constants -from .source_scenario import SourceScenario -from .types import SDKVersion - -def create_agent_path(agents_dir_path: Path, agent_name: str, sdk_version: SDKVersion) -> str: - - agent_path = agents_dir_path / agent_name / sdk_version.value / constants.ENTRY_POINT_NAME - if not agent_path.exists(): - raise FileNotFoundError(f"Agent path does not exist: {agent_path}") - - return str(agent_path.resolve()) - -def create_scenario(agents_dir_path: str | Path, agent_name: str, sdk_version: SDKVersion, delay: float = 30.0) -> Scenario: - - agents_dir_path = Path(agents_dir_path) - agent_path = create_agent_path(agents_dir_path, agent_name, sdk_version) - return SourceScenario( - agent_path, - delay=delay, - ) \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore deleted file mode 100644 index d6deddae7..000000000 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.env -appsettings.* -appsettings.*.json \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE index e69de29bb..187ec681c 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE @@ -0,0 +1,3 @@ +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE index e69de29bb..187ec681c 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE @@ -0,0 +1,3 @@ +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json new file mode 100644 index 000000000..f4cc7bc59 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json @@ -0,0 +1,7 @@ +{ + "agents": { + "my_agent": { + "path": "http://localhost:3978/api/messages" + } + } +} \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE new file mode 100644 index 000000000..187ec681c --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE @@ -0,0 +1,3 @@ +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml new file mode 100644 index 000000000..a3220cdaf --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "e2e-tests" +version = "0.0.0" +requires-python = ">=3.13" +dependencies = [ + "pytest", + "pytest-asyncio", + "microsoft-agents-testing @ git+https://github.com/microsoft/Agents-for-python.git@main#subdirectory=dev/testing/microsoft-agents-testing", +] \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini new file mode 100644 index 000000000..58491634c --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini @@ -0,0 +1,30 @@ +[pytest] +# pytest config for the environments/local test suite. +# Run locally: uv run pytest tests/ +# Or via Docker: ./scripts/run_local.ps1 + +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning + ignore::aiohttp.web.NotAppKeyWarning + +asyncio_mode = auto + +addopts = + --strict-markers + --strict-config + --verbose + --tb=short + --durations=10 + +minversion = 6.0 + +markers = + unit: Unit tests + integration: Integration tests + slow: Slow tests that may take longer to run + requires_network: Tests that require network access + requires_auth: Tests that require authentication + +asyncio_default_fixture_loop_scope = class +asyncio_default_test_loop_scope = class \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py new file mode 100644 index 000000000..11703591e --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py @@ -0,0 +1,3 @@ +from microsoft_agents.testing import scenario_registry + +scenario_registry.load_json("config.json") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py new file mode 100644 index 000000000..14d56fd35 --- /dev/null +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py @@ -0,0 +1,11 @@ +import pytest + +from microsoft_agents.testing import AgentClient + +@pytest.mark.agent_test("my_agent") +async def test_my_agent(agent_client: AgentClient): + + await agent_client.send("Hello, World!", wait=5.0) + + # assert that the agent replied with a message Activity + agent_client.expect().that_for_any(type="message") \ No newline at end of file diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py index 2663f43d4..4e5197cc1 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py @@ -34,7 +34,7 @@ from collections.abc import Iterator from .core import Scenario, ExternalScenario -from .cross_sdk import SourceScenario +from .source_scenario import SourceScenario @dataclass(frozen=True) class ScenarioEntry: diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/source_scenario.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/source_scenario.py similarity index 91% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/source_scenario.py rename to dev/testing/microsoft-agents-testing/microsoft_agents/testing/source_scenario.py index 16ba7d604..1b807a143 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cross_sdk/source_scenario.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/source_scenario.py @@ -13,8 +13,6 @@ from contextlib import asynccontextmanager from microsoft_agents.testing import ( - ActivityTemplate, - ClientConfig, ExternalScenario, ScenarioConfig, ) @@ -22,19 +20,6 @@ from .constants import DEFAULT_LOCAL_AGENT_ENDPOINT -_TEMPLATE = { - "channel_id": "webchat", - "locale": "en-US", - "conversation": {"id": "conv1"}, - "from": {"id": "user1", "name": "User"}, - "recipient": {"id": "bot", "name": "Bot"}, -} - -client_config=ClientConfig( - activity_template=ActivityTemplate(_TEMPLATE) -) - - def _terminate_tree(process: subprocess.Popen, timeout: float = 5.0) -> None: """Terminate `process` and all of its descendants. diff --git a/dev/testing/microsoft-agents-testing/tests/cross_sdk/__init__.py b/dev/testing/microsoft-agents-testing/tests/cross_sdk/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/testing/microsoft-agents-testing/tests/cross_sdk/test_source_scenario.py b/dev/testing/microsoft-agents-testing/tests/test_source_scenario.py similarity index 91% rename from dev/testing/microsoft-agents-testing/tests/cross_sdk/test_source_scenario.py rename to dev/testing/microsoft-agents-testing/tests/test_source_scenario.py index de2889ff5..8c280e622 100644 --- a/dev/testing/microsoft-agents-testing/tests/cross_sdk/test_source_scenario.py +++ b/dev/testing/microsoft-agents-testing/tests/test_source_scenario.py @@ -8,10 +8,8 @@ import pytest -from microsoft_agents.testing.cross_sdk import ( - SourceScenario, - constants -) +from microsoft_agents.testing import SourceScenario +import microsoft_agents.testing.constants as constants class TestSourceScenarioInit: @@ -68,11 +66,11 @@ def _fake_which(name): with patch("shutil.which", side_effect=_fake_which), \ patch("subprocess.Popen", return_value=mock_process) as mock_popen, \ patch( - "microsoft_agents.testing.cross_sdk.source_scenario.asyncio.sleep", + "microsoft_agents.testing.source_scenario.asyncio.sleep", new_callable=AsyncMock, ), \ patch( - "microsoft_agents.testing.cross_sdk.source_scenario._terminate_tree" + "microsoft_agents.testing.source_scenario._terminate_tree" ): async with scenario._run_script(): captured["cmd"] = mock_popen.call_args[0][0] @@ -94,11 +92,11 @@ def _fake_which(name): with patch("shutil.which", side_effect=_fake_which), \ patch("subprocess.Popen", return_value=mock_process) as mock_popen, \ patch( - "microsoft_agents.testing.cross_sdk.source_scenario.asyncio.sleep", + "microsoft_agents.testing.source_scenario.asyncio.sleep", new_callable=AsyncMock, ), \ patch( - "microsoft_agents.testing.cross_sdk.source_scenario._terminate_tree" + "microsoft_agents.testing.source_scenario._terminate_tree" ): async with scenario._run_script(): captured["cmd"] = mock_popen.call_args[0][0] @@ -115,11 +113,11 @@ async def test_script_cwd_is_agent_path(self, tmp_path): with patch("shutil.which", return_value="/usr/bin/pwsh"), \ patch("subprocess.Popen", return_value=mock_process) as mock_popen, \ patch( - "microsoft_agents.testing.cross_sdk.source_scenario.asyncio.sleep", + "microsoft_agents.testing.source_scenario.asyncio.sleep", new_callable=AsyncMock, ), \ patch( - "microsoft_agents.testing.cross_sdk.source_scenario._terminate_tree" + "microsoft_agents.testing.source_scenario._terminate_tree" ): async with scenario._run_script(): pass @@ -146,7 +144,7 @@ def _capture_popen(*args, **kwargs): return proc with patch( - "microsoft_agents.testing.cross_sdk.source_scenario.subprocess.Popen", + "microsoft_agents.testing.source_scenario.subprocess.Popen", side_effect=_capture_popen, ): for run_index in range(2): @@ -206,7 +204,7 @@ def _capture_popen(*args, **kwargs): try: with patch( - "microsoft_agents.testing.cross_sdk.source_scenario.subprocess.Popen", + "microsoft_agents.testing.source_scenario.subprocess.Popen", side_effect=_capture_popen, ): scenario = SourceScenario(str(tmp_path), ps_command) From 6ca09624f5b80850d0bda7ea743da80c899f19bf Mon Sep 17 00:00:00 2001 From: rodrigobr-msft Date: Mon, 15 Jun 2026 14:49:04 -0700 Subject: [PATCH 5/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/testing/scenario_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py index 4e5197cc1..b5fc2b06e 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py @@ -112,7 +112,7 @@ def register( def load_json(self, file_path: str) -> None: """Load scenarios from a JSON file.""" - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) agent_defs = data.get("agents", {}) From 8c46274641dffc6cb5985af916307b8a577a7572 Mon Sep 17 00:00:00 2001 From: rodrigobr-msft Date: Mon, 15 Jun 2026 14:49:13 -0700 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/testing/scenario_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py index b5fc2b06e..8e2876930 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py @@ -123,7 +123,7 @@ def load_json(self, file_path: str) -> None: desc = body.get("description", "") script = body.get("script", "") - if "http" in path_str: + if path_str.startswith(("http://", "https://")): self.register( name, ExternalScenario(path_str), From c5d62724b44a31ca329f48174abf1def2a3a2657 Mon Sep 17 00:00:00 2001 From: rodrigobr-msft Date: Mon, 15 Jun 2026 14:49:48 -0700 Subject: [PATCH 7/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/testing/scenario_registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py index 8e2876930..58fa8de6f 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py @@ -134,9 +134,9 @@ def load_json(self, file_path: str) -> None: if not script: raise ValueError("A 'script' field is required for source scenarios") - path = Path(path_str) + path = (Path(file_path).resolve().parent / path_str).resolve() if not path.exists(): - raise FileNotFoundError(f"Scenario file not found: {path}") + raise FileNotFoundError(f"Agent path not found: {path}") self.register( name, SourceScenario(path, script), From c691fc53fda681863cc97b8af06e1aac71e8b484 Mon Sep 17 00:00:00 2001 From: rodrigobr-msft Date: Mon, 15 Jun 2026 14:49:59 -0700 Subject: [PATCH 8/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/testing/scenario_registry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py index 58fa8de6f..d0876ebeb 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py @@ -212,8 +212,7 @@ def clear(self) -> None: """Remove all registered scenarios. Primarily for testing.""" self._entries.clear() -# Global singlet -# on instance +# Global singleton instance scenario_registry = ScenarioRegistry() From 590da54eb8cb9dea64441cb7792c33dd38d8bb11 Mon Sep 17 00:00:00 2001 From: rodrigobr-msft Date: Mon, 15 Jun 2026 15:04:35 -0700 Subject: [PATCH 9/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/testing/cli/commands/init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py index 086e53d4c..35edc542b 100644 --- a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py +++ b/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py @@ -24,7 +24,7 @@ def _discover_presets() -> dict: for entry in _PRESETS_ROOT.iterdir() if entry.is_dir() } - except Exception: + except (FileNotFoundError, NotADirectoryError): return {}