From d588c370292068f595fa093efc4a399a24402cfb Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 15:52:21 -0800 Subject: [PATCH 01/14] Test plan --- tests/README.md | 27 +++ tests/RUNNING_TESTS.md | 143 ++++++++++++ tests/TEST_PLAN.md | 281 ++++++++++++++++++++++++ tests/runtime/test_environment_utils.py | 163 ++++++++++++++ tests/runtime/test_version_utils.py | 151 +++++++++++++ 5 files changed, 765 insertions(+) create mode 100644 tests/README.md create mode 100644 tests/RUNNING_TESTS.md create mode 100644 tests/TEST_PLAN.md create mode 100644 tests/runtime/test_environment_utils.py create mode 100644 tests/runtime/test_version_utils.py diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..58351b24 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,27 @@ +# Agent365-Python SDK Tests + +Unit and integration tests for the Agent365-Python SDK. This test suite ensures reliability, maintainability, and quality across all SDK modules including runtime, tooling, notifications, and observability extensions. + +## Usage + +For detailed instructions on running tests and generating coverage reports, see: + +- **[Running Tests](RUNNING_TESTS.md)** - Complete guide for installation, running tests, generating coverage reports, and troubleshooting +- **[Test Plan](TEST_PLAN.md)** - Comprehensive testing strategy and implementation roadmap + +## Support + +For issues, questions, or feedback: + +- File issues in the [GitHub Issues](https://github.com/microsoft/Agent365-python/issues) section +- See the [main documentation](../README.md) for more information + +## Trademarks + +*Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653.* + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the MIT License - see the [LICENSE](../LICENSE.md) file for details. diff --git a/tests/RUNNING_TESTS.md b/tests/RUNNING_TESTS.md new file mode 100644 index 00000000..a2769673 --- /dev/null +++ b/tests/RUNNING_TESTS.md @@ -0,0 +1,143 @@ +# Running Unit Tests for Agent365-Python SDK + +## Quick Start + +```bash +# Install dependencies +uv pip install -e libraries/microsoft-agents-a365-runtime +uv pip install pytest pytest-cov pytest-asyncio + +# Run all tests +python -m pytest tests/ + +# Run with coverage +python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=html +``` + +--- + +## Current Test Status + +| Module | Tests | Status | +|--------|-------|--------| +| Runtime | 53 | ✅ Complete | +| Observability | ~20 | ⚠️ Partial | +| Tooling | 0 | ❌ Not Started | +| Notifications | 0 | ❌ Not Started | + +**Coverage Target:** 80%+ | See [TEST_PLAN.md](TEST_PLAN.md) for roadmap + +--- + +## Installation + +```bash +# Runtime module only +uv pip install -e libraries/microsoft-agents-a365-runtime +uv pip install pytest pytest-cov pytest-asyncio + +# All modules +uv pip install -e libraries/microsoft-agents-a365-observability-core +uv pip install -e libraries/microsoft-agents-a365-tooling +uv pip install -e libraries/microsoft-agents-a365-notifications +``` + +--- + +## Running Tests + +```bash +# All tests +python -m pytest tests/ + +# Specific module +python -m pytest tests/runtime/ + +# Specific file +python -m pytest tests/runtime/test_environment_utils.py + +# Specific test +python -m pytest tests/runtime/test_environment_utils.py::TestEnvironmentUtils::test_constants + +# With verbose output +python -m pytest tests/runtime/ -v + +# Stop on first failure +python -m pytest tests/runtime/ -x + +# Pattern matching +python -m pytest tests/runtime/ -k "environment" + +# Re-run failed tests only +python -m pytest --lf +``` + +--- + +## Coverage Reports + +```bash +# Terminal output +python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=term-missing + +# HTML report (opens htmlcov/index.html) +python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=html + +# XML report (for CI/CD tools) +python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=xml +``` + +--- + +## Advanced Options + +```bash +# Parallel execution +uv pip install pytest-xdist +python -m pytest tests/runtime/ -n auto + +# JUnit XML report +python -m pytest tests/runtime/ --junitxml=test-results.xml +``` + +--- + +## Understanding Output + +``` +tests/runtime/test_environment_utils.py::TestEnvironmentUtils::test_constants PASSED [ 1%] +=================================== 53 passed in 0.18s =================================== +``` + +| Status | Description | +|--------|-------------| +| **PASSED** | Test passed successfully | +| **FAILED** | Test failed (shows error details) | +| **SKIPPED** | Test skipped | +| **ERROR** | Error during collection/setup | + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| `ModuleNotFoundError` | `uv pip install -e libraries/microsoft-agents-a365-runtime` | +| `pytest: command not found` | `uv pip install pytest` | +| `ERROR: unrecognized arguments: --cov` | `uv pip install pytest-cov` | + +--- + +## Test Structure + +``` +tests/ +├── README.md # Overview +├── RUNNING_TESTS.md # This file +├── TEST_PLAN.md # Test strategy +└── runtime/ + ├── test_environment_utils.py # 16 tests + ├── test_version_utils.py # 12 tests + ├── test_utility.py # 13 tests + └── test_power_platform_api_discovery.py # 12 tests +``` diff --git a/tests/TEST_PLAN.md b/tests/TEST_PLAN.md new file mode 100644 index 00000000..8b8268f3 --- /dev/null +++ b/tests/TEST_PLAN.md @@ -0,0 +1,281 @@ +# Test Plan for Agent365-Python SDK + +**Version:** 1.0 +**Date:** November 24, 2025 +**Status:** Draft +**Owner:** Team Review Required + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Testing Strategy](#testing-strategy) +3. [Phase 1: Unit Tests](#phase-1-unit-tests) +4. [Phase 2: Integration Tests](#phase-2-integration-tests) +5. [Phase 3: CI/CD Integration](#phase-3-cicd-integration) +6. [Success Criteria](#success-criteria) +7. [Implementation Roadmap](#implementation-roadmap) + +--- + +## Overview + +### Purpose +Establish comprehensive test coverage for the Agent365-Python SDK to ensure reliability, maintainability, and quality across all modules. + +### Current State +- ✅ Some unit tests exist for `observability` module +- ⚠️ Partial coverage for `runtime` module +- ❌ Missing tests for `tooling` and `notifications` modules +- ⚠️ Limited integration test coverage +- ❌ No automated coverage reporting in CI + +### Goals +1. Achieve **80%+ code coverage** across all modules +2. Implement unit tests following Python best practices (`unittest` framework) +3. Create integration tests for cross-module functionality +4. Integrate testing into CI/CD pipeline with automated coverage reporting +5. Establish testing standards for future development + +--- + +## Testing Strategy + +### Testing Pyramid + +``` + /\ + / \ Integration Tests + / \ - Module interactions + / \ - Mocked external services + /--------\ + / \ Unit Tests + / \ - Isolated function/class testing +---------------- - 80%+ coverage target +``` + +### Tools & Framework +- **Framework:** `unittest` + `pytest` runner +- **Coverage:** `pytest-cov` +- **Mocking:** `unittest.mock` +- **Async:** `pytest-asyncio` + +### Testing Principles +- **AAA Pattern:** Arrange → Act → Assert +- **FIRST:** Fast, Independent, Repeatable, Self-validating, Timely +- **Naming:** `test___` + +--- + +## Phase 1: Unit Tests + +### Phase 1.1: Runtime Module +**Priority:** HIGH + +| Module | Test File | Status | Priority | +|--------|-----------|--------|----------| +| `power_platform_api_discovery.py` | `test_power_platform_api_discovery.py` | ✅ Exists | Review & Expand | +| `utility.py` | `test_utility.py` | ✅ Exists | Review & Expand | +| `environment_utils.py` | `test_environment_utils.py` | ❌ Missing | **HIGH** | +| `version_utils.py` | `test_version_utils.py` | ❌ Missing | Medium | + +**Key Areas to Test:** +- Environment detection and configuration +- Authentication scope resolution +- Version utilities with deprecation warnings +- Power Platform API discovery + +--- + +### Phase 1.2: Tooling Module +**Priority:** HIGH + +**Directory Structure:** +``` +tests/tooling/ +├── utils/test_utility.py +├── models/test_mcp_server_config.py +└── services/test_mcp_tool_server_configuration_service.py +``` + +**Key Areas to Test:** +- MCP server configuration and validation +- Environment-based URL generation +- Tools mode handling (Mock vs Platform) +- Gateway discovery and authentication +- Manifest file parsing + +--- + +### Phase 1.3: Notifications Module +**Priority:** HIGH + +**Directory Structure:** +``` +tests/notifications/ +├── models/ +│ ├── test_agent_lifecycle_event.py +│ ├── test_agent_notification_activity.py +│ ├── test_email_reference.py +│ └── test_notification_types.py +└── test_agent_notification.py +``` + +**Key Areas to Test:** +- Activity parsing and entity extraction +- Notification routing and filtering +- Decorator functionality +- Channel and subchannel handling + +--- + +### Phase 1.4: Observability Extensions +**Priority:** MEDIUM + +| Extension | Action | Priority | +|-----------|--------|----------| +| `agentframework` | Expand existing tests | Medium | +| `langchain` | Expand existing tests | Medium | +| `openai` | Expand existing tests | Medium | +| `semantickernel` | Expand existing tests | Medium | + +**Key Areas to Test:** +- Wrapper functionality +- Trace processing +- Event handling + +--- + +### Phase 1.5: Tooling Extensions +**Priority:** LOW + +**Extensions to Test:** +- Agent Framework tooling integration +- Azure AI Foundry tooling integration +- OpenAI tooling integration +- Semantic Kernel tooling integration + +--- + +## Phase 2: Integration Tests + +### Phase 2.1: Module Integration +**Priority:** HIGH + +**Test Scenarios:** +- Runtime + Observability integration +- Tooling + Runtime integration +- Notifications + Runtime integration +- End-to-end workflow scenarios + +**Focus:** +- Cross-module interactions +- Mocked external dependencies +- Configuration propagation +- Error handling across boundaries + +--- + +### Phase 2.2: Extension Integration +**Priority:** MEDIUM + +**Test Scenarios:** +- Agent Framework full flow +- LangChain full flow +- OpenAI Agents full flow +- Semantic Kernel full flow + +**Focus:** +- End-to-end agent execution with observability +- Tool invocation with MCP servers +- Notification delivery +- Cross-extension compatibility + +--- + +## Phase 3: CI/CD Integration + +### Phase 3.1: Test Automation +**Priority:** HIGH + +**Setup:** +- GitHub Actions workflow for automated testing +- Multi-version Python matrix (3.9, 3.10, 3.11, 3.12) +- Automated coverage reporting with Codecov +- PR blocking on test failures + +--- + +### Phase 3.2: Coverage Requirements +**Priority:** HIGH + +**Configuration:** +- Minimum 80% code coverage enforcement +- Coverage reports in XML, HTML, and terminal formats +- Branch protection rules requiring passing tests +- Coverage trend tracking + +--- + +### Phase 3.3: Pre-commit Hooks +**Priority:** MEDIUM + +**Setup:** +- Code formatting checks (ruff) +- Test execution before commit +- YAML validation +- Trailing whitespace cleanup + +--- + +## Success Criteria + +### Phase 1: Unit Tests +- ✅ 80%+ code coverage for all modules +- ✅ All tests follow AAA pattern +- ✅ Tests run independently in any order +- ✅ Full suite completes in < 30 seconds + +### Phase 2: Integration Tests +- ✅ All major integration points tested +- ✅ 99%+ test reliability +- ✅ External services properly mocked +- ✅ Integration scenarios documented + +### Phase 3: CI/CD +- ✅ Automated test execution on all PRs +- ✅ Coverage reports visible and enforced +- ✅ PR merge blocked on test failures or coverage drops +- ✅ Tests pass on Python 3.9-3.12 +- ✅ Full suite completes in < 5 minutes + +--- + +## Implementation Roadmap + +| Phase | Focus | Deliverables | Owner | +|-------|-------|-------------|-------| +| 1.1 | Runtime Module | `test_environment_utils.py`, `test_version_utils.py` | TBD | +| 1.2 | Tooling Module | All tooling test files | TBD | +| 1.3 | Notifications Module | All notifications test files | TBD | +| 1.4 | Observability Extensions | Expand existing tests | TBD | +| 1.5 | Tooling Extensions | Extension test files | TBD | +| 2.1 | Module Integration | Integration test suite | TBD | +| 2.2 | Extension Integration | Full flow tests | TBD | +| 3 | CI/CD Setup | GitHub Actions, coverage, pre-commit | TBD | + +### Key Milestones +- **M1:** Runtime tests complete, baseline coverage established +- **M2:** All core module unit tests complete +- **M3:** Integration test framework in place +- **M4:** Full CI/CD integration, coverage enforcement + +--- + +## References + +- [Python unittest Documentation](https://docs.python.org/3/library/unittest.html) +- [pytest Documentation](https://docs.pytest.org/) +- [unittest.mock Guide](https://docs.python.org/3/library/unittest.mock.html) +- Existing tests: `tests/observability/core/` and `tests/runtime/` diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py new file mode 100644 index 00000000..e5aa1955 --- /dev/null +++ b/tests/runtime/test_environment_utils.py @@ -0,0 +1,163 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Unit tests for environment_utils module.""" + +import os +import unittest +from unittest.mock import patch + +from microsoft_agents_a365.runtime.environment_utils import ( + DEVELOPMENT_ENVIRONMENT_NAME, + PROD_OBSERVABILITY_CLUSTER_CATEGORY, + PROD_OBSERVABILITY_SCOPE, + PRODUCTION_ENVIRONMENT_NAME, + get_observability_authentication_scope, + is_development_environment, +) + + +class TestEnvironmentUtils(unittest.TestCase): + """Test cases for environment utility functions.""" + + def tearDown(self): + """Clean up environment variables after each test.""" + env_vars = ["PYTHON_ENVIRONMENT"] + for var in env_vars: + if var in os.environ: + del os.environ[var] + + def test_constants(self): + """Test that environment constants have expected values.""" + self.assertEqual(PROD_OBSERVABILITY_SCOPE, "https://api.powerplatform.com/.default") + self.assertEqual(PROD_OBSERVABILITY_CLUSTER_CATEGORY, "prod") + self.assertEqual(PRODUCTION_ENVIRONMENT_NAME, "production") + self.assertEqual(DEVELOPMENT_ENVIRONMENT_NAME, "Development") + + def test_get_observability_authentication_scope_returns_prod_scope(self): + """Test that get_observability_authentication_scope returns production scope.""" + result = get_observability_authentication_scope() + + self.assertIsInstance(result, list) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], PROD_OBSERVABILITY_SCOPE) + + def test_is_development_environment_with_no_env_var(self): + """Test is_development_environment returns False when no environment variable is set.""" + # Ensure environment variables are not set + if "PYTHON_ENVIRONMENT" in os.environ: + del os.environ["PYTHON_ENVIRONMENT"] + + result = is_development_environment() + + self.assertFalse(result) + + def test_is_development_environment_with_development_env_var(self): + """Test is_development_environment returns True when PYTHON_ENVIRONMENT is 'Development'.""" + os.environ["PYTHON_ENVIRONMENT"] = "Development" + + result = is_development_environment() + + self.assertTrue(result) + + def test_is_development_environment_case_insensitive(self): + """Test is_development_environment is case-insensitive.""" + test_cases = ["development", "DEVELOPMENT", "DeveLoPMenT", "Development"] + + for env_value in test_cases: + with self.subTest(env_value=env_value): + os.environ["PYTHON_ENVIRONMENT"] = env_value + result = is_development_environment() + self.assertTrue(result) + + def test_is_development_environment_with_production_env_var(self): + """Test is_development_environment returns False when PYTHON_ENVIRONMENT is 'production'.""" + os.environ["PYTHON_ENVIRONMENT"] = "production" + + result = is_development_environment() + + self.assertFalse(result) + + def test_is_development_environment_with_other_env_var(self): + """Test is_development_environment returns False for other environment values.""" + test_cases = ["staging", "test", "preprod", "custom"] + + for env_value in test_cases: + with self.subTest(env_value=env_value): + os.environ["PYTHON_ENVIRONMENT"] = env_value + result = is_development_environment() + self.assertFalse(result) + + def test_is_development_environment_with_empty_env_var(self): + """Test is_development_environment returns False when PYTHON_ENVIRONMENT is empty.""" + os.environ["PYTHON_ENVIRONMENT"] = "" + + result = is_development_environment() + + self.assertFalse(result) + + def test_is_development_environment_with_whitespace_env_var(self): + """Test is_development_environment returns False when PYTHON_ENVIRONMENT is whitespace.""" + os.environ["PYTHON_ENVIRONMENT"] = " " + + result = is_development_environment() + + self.assertFalse(result) + + @patch.dict(os.environ, {"PYTHON_ENVIRONMENT": "Development"}, clear=False) + def test_python_environment_precedence(self): + """Test that PYTHON_ENVIRONMENT takes precedence.""" + result = is_development_environment() + + self.assertTrue(result) + + def test_default_environment_is_production(self): + """Test that the default environment is production when no env vars are set.""" + # Ensure no environment variables are set + if "PYTHON_ENVIRONMENT" in os.environ: + del os.environ["PYTHON_ENVIRONMENT"] + + # The _get_current_environment function should default to PRODUCTION_ENVIRONMENT_NAME + result = is_development_environment() + + self.assertFalse(result) + + +class TestObservabilityAuthenticationScope(unittest.TestCase): + """Test cases for observability authentication scope.""" + + def test_scope_is_list(self): + """Test that the scope is returned as a list.""" + result = get_observability_authentication_scope() + + self.assertIsInstance(result, list) + + def test_scope_contains_single_value(self): + """Test that the scope list contains exactly one value.""" + result = get_observability_authentication_scope() + + self.assertEqual(len(result), 1) + + def test_scope_value_is_string(self): + """Test that the scope value is a string.""" + result = get_observability_authentication_scope() + + self.assertIsInstance(result[0], str) + + def test_scope_value_format(self): + """Test that the scope value has the correct format.""" + result = get_observability_authentication_scope() + + self.assertTrue(result[0].startswith("https://")) + self.assertTrue(result[0].endswith(".default")) + + def test_scope_consistency(self): + """Test that multiple calls return the same scope.""" + result1 = get_observability_authentication_scope() + result2 = get_observability_authentication_scope() + + self.assertEqual(result1, result2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/runtime/test_version_utils.py b/tests/runtime/test_version_utils.py new file mode 100644 index 00000000..a038cf50 --- /dev/null +++ b/tests/runtime/test_version_utils.py @@ -0,0 +1,151 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Unit tests for version_utils module.""" + +import os +import unittest +import warnings + +from microsoft_agents_a365.runtime.version_utils import build_version + + +class TestVersionUtils(unittest.TestCase): + """Test cases for version utility functions.""" + + def tearDown(self): + """Clean up environment variables after each test.""" + if "AGENT365_PYTHON_SDK_PACKAGE_VERSION" in os.environ: + del os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] + + def test_build_version_returns_string(self): + """Test that build_version returns a string.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = build_version() + + self.assertIsInstance(result, str) + + def test_build_version_default_value(self): + """Test build_version returns default version when no env var is set.""" + # Ensure environment variable is not set + if "AGENT365_PYTHON_SDK_PACKAGE_VERSION" in os.environ: + del os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = build_version() + + self.assertEqual(result, "0.0.0") + + def test_build_version_with_env_var(self): + """Test build_version returns version from environment variable.""" + test_version = "1.2.3" + os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = test_version + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = build_version() + + self.assertEqual(result, test_version) + + def test_build_version_with_complex_version(self): + """Test build_version with complex version strings.""" + test_cases = [ + "1.0.0-alpha", + "2.3.4-beta.1", + "3.0.0-rc.2", + "1.2.3+build.456", + "2.0.0-alpha.1+build.789", + ] + + for version in test_cases: + with self.subTest(version=version): + os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = version + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = build_version() + + self.assertEqual(result, version) + + def test_build_version_with_empty_env_var(self): + """Test build_version with empty environment variable.""" + os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = "" + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = build_version() + + self.assertEqual(result, "") + + def test_build_version_deprecation_warning(self): + """Test that build_version raises DeprecationWarning.""" + with self.assertWarns(DeprecationWarning) as cm: + build_version() + + warning_message = str(cm.warning) + self.assertIn("deprecated", warning_message.lower()) + self.assertIn("setuptools-git-versioning", warning_message) + + def test_build_version_deprecation_warning_message(self): + """Test that deprecation warning contains the correct message.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + build_version() + + # Check that exactly one warning was raised + self.assertEqual(len(w), 1) + + # Check that it's a DeprecationWarning + self.assertTrue(issubclass(w[0].category, DeprecationWarning)) + + # Check the message content + message = str(w[0].message) + self.assertIn("build_version() is deprecated", message) + self.assertIn("setuptools-git-versioning", message) + + def test_build_version_stacklevel(self): + """Test that deprecation warning has correct stack level.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + build_version() + + # The warning should point to the caller of build_version + # not to the function itself (stacklevel=2) + self.assertEqual(len(w), 1) + warning = w[0] + + # Verify the warning was captured + self.assertIsNotNone(warning.filename) + self.assertGreater(warning.lineno, 0) + + +class TestVersionUtilsDocstring(unittest.TestCase): + """Test cases for version_utils module documentation.""" + + def test_module_has_docstring(self): + """Test that the module has a docstring.""" + import microsoft_agents_a365.runtime.version_utils as version_utils + + self.assertIsNotNone(version_utils.__doc__) + self.assertGreater(len(version_utils.__doc__), 0) + + def test_module_docstring_mentions_deprecation(self): + """Test that module docstring mentions deprecation.""" + import microsoft_agents_a365.runtime.version_utils as version_utils + + self.assertIn("deprecated", version_utils.__doc__.lower()) + + def test_function_has_docstring(self): + """Test that build_version function has a docstring.""" + self.assertIsNotNone(build_version.__doc__) + self.assertGreater(len(build_version.__doc__), 0) + + def test_function_docstring_mentions_deprecation(self): + """Test that function docstring mentions deprecation.""" + self.assertIn("DEPRECATED", build_version.__doc__) + + +if __name__ == "__main__": + unittest.main() From 723a21ebc55c4fad8d1630bc91f050dc35c6e8dd Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 16:32:10 -0800 Subject: [PATCH 02/14] Fix running tests instructions --- tests/RUNNING_TESTS.md | 182 ++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/tests/RUNNING_TESTS.md b/tests/RUNNING_TESTS.md index a2769673..a8652f5c 100644 --- a/tests/RUNNING_TESTS.md +++ b/tests/RUNNING_TESTS.md @@ -1,143 +1,143 @@ # Running Unit Tests for Agent365-Python SDK -## Quick Start +This guide covers setting up and running tests in Visual Studio Code. -```bash -# Install dependencies -uv pip install -e libraries/microsoft-agents-a365-runtime -uv pip install pytest pytest-cov pytest-asyncio +--- -# Run all tests -python -m pytest tests/ +## Prerequisites -# Run with coverage -python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=html -``` +- **Python 3.11+** installed +- **Visual Studio Code** with Python extension +- **Git** repository cloned locally --- -## Current Test Status +## Test Structure -| Module | Tests | Status | -|--------|-------|--------| -| Runtime | 53 | ✅ Complete | -| Observability | ~20 | ⚠️ Partial | -| Tooling | 0 | ❌ Not Started | -| Notifications | 0 | ❌ Not Started | +> **Note:** This structure will be updated as new tests are added. -**Coverage Target:** 80%+ | See [TEST_PLAN.md](TEST_PLAN.md) for roadmap +```plaintext +tests/ +├── runtime/ # Runtime tests +├── observability/ # Observability tests +├── tooling/ # Tooling tests +└── notifications/ # Notifications tests +``` --- -## Installation +## Initial Setup -```bash -# Runtime module only -uv pip install -e libraries/microsoft-agents-a365-runtime -uv pip install pytest pytest-cov pytest-asyncio - -# All modules -uv pip install -e libraries/microsoft-agents-a365-observability-core -uv pip install -e libraries/microsoft-agents-a365-tooling -uv pip install -e libraries/microsoft-agents-a365-notifications -``` +### 1. Configure Python Environment ---- +1. Press `Ctrl+Shift+P` +2. Type "Python: Select Interpreter" +3. Choose your Python 3.11+ interpreter -## Running Tests +### 2. Install Dependencies -```bash -# All tests -python -m pytest tests/ +```powershell +# Install test dependencies +uv pip install pytest pytest-asyncio pytest-mock pytest-cov pytest-html -# Specific module -python -m pytest tests/runtime/ +# Install workspace packages +uv pip install -e . +``` -# Specific file -python -m pytest tests/runtime/test_environment_utils.py +--- -# Specific test -python -m pytest tests/runtime/test_environment_utils.py::TestEnvironmentUtils::test_constants +## Running Tests in VS Code -# With verbose output -python -m pytest tests/runtime/ -v +### Test Explorer -# Stop on first failure -python -m pytest tests/runtime/ -x +1. Click the beaker icon in the Activity Bar or press `Ctrl+Shift+P` → "Test: Focus on Test Explorer View" +2. Click the play button to run tests (all/folder/file/individual) +3. Right-click → "Debug Test" to debug with breakpoints -# Pattern matching -python -m pytest tests/runtime/ -k "environment" +### Command Palette -# Re-run failed tests only -python -m pytest --lf -``` +- `Test: Run All Tests` +- `Test: Run Tests in Current File` +- `Test: Debug Tests in Current File` --- -## Coverage Reports +## Running Tests from Command Line -```bash -# Terminal output -python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=term-missing +```powershell +# Run all tests +python -m pytest tests/ -# HTML report (opens htmlcov/index.html) -python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=html +# Run specific module/file +python -m pytest tests/runtime/ +python -m pytest tests/runtime/test_environment_utils.py -# XML report (for CI/CD tools) -python -m pytest tests/runtime/ --cov=microsoft_agents_a365.runtime --cov-report=xml +# Run with options +python -m pytest tests/ -v # Verbose +python -m pytest tests/ -x # Stop on first failure +python -m pytest tests/ -k "environment" # Pattern matching +python -m pytest --lf # Re-run failed tests ``` --- -## Advanced Options +## Generating Reports -```bash -# Parallel execution -uv pip install pytest-xdist -python -m pytest tests/runtime/ -n auto +### HTML Reports -# JUnit XML report -python -m pytest tests/runtime/ --junitxml=test-results.xml -``` - ---- +```powershell +# Coverage report +python -m pytest tests/ --cov=libraries --cov-report=html +Invoke-Item htmlcov\index.html -## Understanding Output +# Test report +python -m pytest tests/ --html=test-report.html --self-contained-html +Invoke-Item test-report.html +# Combined +python -m pytest tests/ --cov=libraries --cov-report=html --html=test-report.html --self-contained-html -v ``` -tests/runtime/test_environment_utils.py::TestEnvironmentUtils::test_constants PASSED [ 1%] -=================================== 53 passed in 0.18s =================================== -``` -| Status | Description | -|--------|-------------| -| **PASSED** | Test passed successfully | -| **FAILED** | Test failed (shows error details) | -| **SKIPPED** | Test skipped | -| **ERROR** | Error during collection/setup | +### CI/CD Reports + +```powershell +# XML reports for CI/CD pipelines +python -m pytest tests/ --cov=libraries --cov-report=xml --junitxml=test-results.xml +``` --- ## Troubleshooting +### Common Issues + | Issue | Solution | |-------|----------| -| `ModuleNotFoundError` | `uv pip install -e libraries/microsoft-agents-a365-runtime` | -| `pytest: command not found` | `uv pip install pytest` | -| `ERROR: unrecognized arguments: --cov` | `uv pip install pytest-cov` | +| **Test loading failed** | Clean pyproject.toml, reinstall packages, restart VS Code | +| **ImportError: No module named 'pytest'** | `uv pip install pytest pytest-asyncio pytest-mock` | +| **ImportError: No module named 'microsoft_agents_a365'** | `uv pip install -e .` | +| **Tests not discovered** | Refresh Test Explorer or restart VS Code | ---- +### Fix Steps -## Test Structure +If tests fail to discover or import errors occur: +**1. Clean pyproject.toml** + +```powershell +$content = Get-Content "pyproject.toml" -Raw +$fixed = $content -replace "`r`n", "`n" +$fixed | Set-Content "pyproject.toml" -NoNewline ``` -tests/ -├── README.md # Overview -├── RUNNING_TESTS.md # This file -├── TEST_PLAN.md # Test strategy -└── runtime/ - ├── test_environment_utils.py # 16 tests - ├── test_version_utils.py # 12 tests - ├── test_utility.py # 13 tests - └── test_power_platform_api_discovery.py # 12 tests + +**2. Reinstall packages** + +```powershell +uv pip install -e . ``` + +**3. Restart VS Code** + +- Close completely and reopen +- Wait for Python extension to reload +- Refresh Test Explorer From 91e3f51cfffcaa738f027178b8c4bb0df9e3f52c Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 16:35:35 -0800 Subject: [PATCH 03/14] fix Readme --- tests/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/README.md b/tests/README.md index 58351b24..5a420cba 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,13 +1,13 @@ -# Agent365-Python SDK Tests +# Microsoft Agent 365 SDK Tests -Unit and integration tests for the Agent365-Python SDK. This test suite ensures reliability, maintainability, and quality across all SDK modules including runtime, tooling, notifications, and observability extensions. +Unit and integration tests for the Microsoft Agent 365-Python SDK. This test suite ensures reliability, maintainability, and quality across all modules including runtime, tooling, notifications, and observability extensions. ## Usage For detailed instructions on running tests and generating coverage reports, see: -- **[Running Tests](RUNNING_TESTS.md)** - Complete guide for installation, running tests, generating coverage reports, and troubleshooting - **[Test Plan](TEST_PLAN.md)** - Comprehensive testing strategy and implementation roadmap +- **[Running Tests](RUNNING_TESTS.md)** - Complete guide for installation, running tests, generating coverage reports, and troubleshooting ## Support From 2a927e1a9c8bf79154419e158be65a02ec18a41f Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 17:00:37 -0800 Subject: [PATCH 04/14] fix --- .gitignore | 9 +++++ tests/RUNNING_TESTS.md | 76 +++++++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 7dc57dc7..4099784f 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,15 @@ build/ .pytest_cache/ _version.py +# Test coverage and reports +htmlcov/ +.coverage +coverage.xml +test-report.html +test-results.xml +*.cover +.hypothesis/ + # Virtual environments .venv/ venv/ diff --git a/tests/RUNNING_TESTS.md b/tests/RUNNING_TESTS.md index a8652f5c..9ded70f4 100644 --- a/tests/RUNNING_TESTS.md +++ b/tests/RUNNING_TESTS.md @@ -1,14 +1,39 @@ # Running Unit Tests for Agent365-Python SDK -This guide covers setting up and running tests in Visual Studio Code. +This guide covers setting up and running tests. --- ## Prerequisites -- **Python 3.11+** installed -- **Visual Studio Code** with Python extension -- **Git** repository cloned locally +### 1. Create Virtual Environment + +```powershell +# Create virtual environment using uv +uv venv + +# Activate the virtual environment +.\.venv\Scripts\Activate.ps1 +``` + +### 2. Configure Python Environment + +1. Press `Ctrl+Shift+P` +2. Type "Python: Select Interpreter" +3. Choose the `.venv` interpreter from the list + +### 3. Install Dependencies + +```powershell +# Test framework and reporting +uv pip install pytest pytest-asyncio pytest-mock pytest-cov pytest-html wrapt + +# SDK core libraries +uv pip install -e libraries/microsoft-agents-a365-runtime -e libraries/microsoft-agents-a365-notifications -e libraries/microsoft-agents-a365-observability-core -e libraries/microsoft-agents-a365-tooling + +# Framework extension libraries +uv pip install -e libraries/microsoft-agents-a365-observability-extensions-langchain -e libraries/microsoft-agents-a365-observability-extensions-openai -e libraries/microsoft-agents-a365-observability-extensions-semantickernel -e libraries/microsoft-agents-a365-observability-extensions-agentframework -e libraries/microsoft-agents-a365-tooling-extensions-agentframework -e libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry -e libraries/microsoft-agents-a365-tooling-extensions-openai -e libraries/microsoft-agents-a365-tooling-extensions-semantickernel +``` --- @@ -26,27 +51,7 @@ tests/ --- -## Initial Setup - -### 1. Configure Python Environment - -1. Press `Ctrl+Shift+P` -2. Type "Python: Select Interpreter" -3. Choose your Python 3.11+ interpreter - -### 2. Install Dependencies - -```powershell -# Install test dependencies -uv pip install pytest pytest-asyncio pytest-mock pytest-cov pytest-html - -# Install workspace packages -uv pip install -e . -``` - ---- - -## Running Tests in VS Code +## Running Tests in VS Code (Optional) ### Test Explorer @@ -87,15 +92,22 @@ python -m pytest --lf # Re-run failed tests ```powershell # Coverage report -python -m pytest tests/ --cov=libraries --cov-report=html -Invoke-Item htmlcov\index.html +python -m pytest tests/ --cov=libraries --cov-report=html -v + +# View reports +start htmlcov\index.html -# Test report +# Test report (requires: uv pip install pytest-html) python -m pytest tests/ --html=test-report.html --self-contained-html -Invoke-Item test-report.html -# Combined +# View reports +start test-report.html + +# Combined (requires: uv pip install pytest-html) python -m pytest tests/ --cov=libraries --cov-report=html --html=test-report.html --self-contained-html -v + +# View reports +start htmlcov\index.html ``` ### CI/CD Reports @@ -103,6 +115,10 @@ python -m pytest tests/ --cov=libraries --cov-report=html --html=test-report.htm ```powershell # XML reports for CI/CD pipelines python -m pytest tests/ --cov=libraries --cov-report=xml --junitxml=test-results.xml + +# View reports +start test-results.xml +start coverage.xml ``` --- From 84169ba9358f786f135aa25b4e3af213c162360e Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 17:02:37 -0800 Subject: [PATCH 05/14] format fix --- tests/runtime/test_environment_utils.py | 42 ++++++++++++------------- tests/runtime/test_version_utils.py | 36 ++++++++++----------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index e5aa1955..8710ae99 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -37,7 +37,7 @@ def test_constants(self): def test_get_observability_authentication_scope_returns_prod_scope(self): """Test that get_observability_authentication_scope returns production scope.""" result = get_observability_authentication_scope() - + self.assertIsInstance(result, list) self.assertEqual(len(result), 1) self.assertEqual(result[0], PROD_OBSERVABILITY_SCOPE) @@ -47,23 +47,23 @@ def test_is_development_environment_with_no_env_var(self): # Ensure environment variables are not set if "PYTHON_ENVIRONMENT" in os.environ: del os.environ["PYTHON_ENVIRONMENT"] - + result = is_development_environment() - + self.assertFalse(result) def test_is_development_environment_with_development_env_var(self): """Test is_development_environment returns True when PYTHON_ENVIRONMENT is 'Development'.""" os.environ["PYTHON_ENVIRONMENT"] = "Development" - + result = is_development_environment() - + self.assertTrue(result) def test_is_development_environment_case_insensitive(self): """Test is_development_environment is case-insensitive.""" test_cases = ["development", "DEVELOPMENT", "DeveLoPMenT", "Development"] - + for env_value in test_cases: with self.subTest(env_value=env_value): os.environ["PYTHON_ENVIRONMENT"] = env_value @@ -73,15 +73,15 @@ def test_is_development_environment_case_insensitive(self): def test_is_development_environment_with_production_env_var(self): """Test is_development_environment returns False when PYTHON_ENVIRONMENT is 'production'.""" os.environ["PYTHON_ENVIRONMENT"] = "production" - + result = is_development_environment() - + self.assertFalse(result) def test_is_development_environment_with_other_env_var(self): """Test is_development_environment returns False for other environment values.""" test_cases = ["staging", "test", "preprod", "custom"] - + for env_value in test_cases: with self.subTest(env_value=env_value): os.environ["PYTHON_ENVIRONMENT"] = env_value @@ -91,24 +91,24 @@ def test_is_development_environment_with_other_env_var(self): def test_is_development_environment_with_empty_env_var(self): """Test is_development_environment returns False when PYTHON_ENVIRONMENT is empty.""" os.environ["PYTHON_ENVIRONMENT"] = "" - + result = is_development_environment() - + self.assertFalse(result) def test_is_development_environment_with_whitespace_env_var(self): """Test is_development_environment returns False when PYTHON_ENVIRONMENT is whitespace.""" os.environ["PYTHON_ENVIRONMENT"] = " " - + result = is_development_environment() - + self.assertFalse(result) @patch.dict(os.environ, {"PYTHON_ENVIRONMENT": "Development"}, clear=False) def test_python_environment_precedence(self): """Test that PYTHON_ENVIRONMENT takes precedence.""" result = is_development_environment() - + self.assertTrue(result) def test_default_environment_is_production(self): @@ -116,10 +116,10 @@ def test_default_environment_is_production(self): # Ensure no environment variables are set if "PYTHON_ENVIRONMENT" in os.environ: del os.environ["PYTHON_ENVIRONMENT"] - + # The _get_current_environment function should default to PRODUCTION_ENVIRONMENT_NAME result = is_development_environment() - + self.assertFalse(result) @@ -129,25 +129,25 @@ class TestObservabilityAuthenticationScope(unittest.TestCase): def test_scope_is_list(self): """Test that the scope is returned as a list.""" result = get_observability_authentication_scope() - + self.assertIsInstance(result, list) def test_scope_contains_single_value(self): """Test that the scope list contains exactly one value.""" result = get_observability_authentication_scope() - + self.assertEqual(len(result), 1) def test_scope_value_is_string(self): """Test that the scope value is a string.""" result = get_observability_authentication_scope() - + self.assertIsInstance(result[0], str) def test_scope_value_format(self): """Test that the scope value has the correct format.""" result = get_observability_authentication_scope() - + self.assertTrue(result[0].startswith("https://")) self.assertTrue(result[0].endswith(".default")) @@ -155,7 +155,7 @@ def test_scope_consistency(self): """Test that multiple calls return the same scope.""" result1 = get_observability_authentication_scope() result2 = get_observability_authentication_scope() - + self.assertEqual(result1, result2) diff --git a/tests/runtime/test_version_utils.py b/tests/runtime/test_version_utils.py index a038cf50..dd5a75e8 100644 --- a/tests/runtime/test_version_utils.py +++ b/tests/runtime/test_version_utils.py @@ -23,7 +23,7 @@ def test_build_version_returns_string(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) result = build_version() - + self.assertIsInstance(result, str) def test_build_version_default_value(self): @@ -31,22 +31,22 @@ def test_build_version_default_value(self): # Ensure environment variable is not set if "AGENT365_PYTHON_SDK_PACKAGE_VERSION" in os.environ: del os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] - + with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) result = build_version() - + self.assertEqual(result, "0.0.0") def test_build_version_with_env_var(self): """Test build_version returns version from environment variable.""" test_version = "1.2.3" os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = test_version - + with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) result = build_version() - + self.assertEqual(result, test_version) def test_build_version_with_complex_version(self): @@ -58,32 +58,32 @@ def test_build_version_with_complex_version(self): "1.2.3+build.456", "2.0.0-alpha.1+build.789", ] - + for version in test_cases: with self.subTest(version=version): os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = version - + with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) result = build_version() - + self.assertEqual(result, version) def test_build_version_with_empty_env_var(self): """Test build_version with empty environment variable.""" os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = "" - + with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) result = build_version() - + self.assertEqual(result, "") def test_build_version_deprecation_warning(self): """Test that build_version raises DeprecationWarning.""" with self.assertWarns(DeprecationWarning) as cm: build_version() - + warning_message = str(cm.warning) self.assertIn("deprecated", warning_message.lower()) self.assertIn("setuptools-git-versioning", warning_message) @@ -93,13 +93,13 @@ def test_build_version_deprecation_warning_message(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") build_version() - + # Check that exactly one warning was raised self.assertEqual(len(w), 1) - + # Check that it's a DeprecationWarning self.assertTrue(issubclass(w[0].category, DeprecationWarning)) - + # Check the message content message = str(w[0].message) self.assertIn("build_version() is deprecated", message) @@ -110,12 +110,12 @@ def test_build_version_stacklevel(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") build_version() - + # The warning should point to the caller of build_version # not to the function itself (stacklevel=2) self.assertEqual(len(w), 1) warning = w[0] - + # Verify the warning was captured self.assertIsNotNone(warning.filename) self.assertGreater(warning.lineno, 0) @@ -127,14 +127,14 @@ class TestVersionUtilsDocstring(unittest.TestCase): def test_module_has_docstring(self): """Test that the module has a docstring.""" import microsoft_agents_a365.runtime.version_utils as version_utils - + self.assertIsNotNone(version_utils.__doc__) self.assertGreater(len(version_utils.__doc__), 0) def test_module_docstring_mentions_deprecation(self): """Test that module docstring mentions deprecation.""" import microsoft_agents_a365.runtime.version_utils as version_utils - + self.assertIn("deprecated", version_utils.__doc__.lower()) def test_function_has_docstring(self): From 70f4c08148060cf3a4999cf7b013fc4f2663fa1f Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 17:10:17 -0800 Subject: [PATCH 06/14] fix plan --- tests/TEST_PLAN.md | 293 ++++++++++++--------------------------------- 1 file changed, 79 insertions(+), 214 deletions(-) diff --git a/tests/TEST_PLAN.md b/tests/TEST_PLAN.md index 8b8268f3..c8fe3e3d 100644 --- a/tests/TEST_PLAN.md +++ b/tests/TEST_PLAN.md @@ -1,281 +1,146 @@ # Test Plan for Agent365-Python SDK +> **Note:** This plan is under active development. Keep updating as testing progresses. + **Version:** 1.0 **Date:** November 24, 2025 -**Status:** Draft -**Owner:** Team Review Required - ---- - -## Table of Contents - -1. [Overview](#overview) -2. [Testing Strategy](#testing-strategy) -3. [Phase 1: Unit Tests](#phase-1-unit-tests) -4. [Phase 2: Integration Tests](#phase-2-integration-tests) -5. [Phase 3: CI/CD Integration](#phase-3-cicd-integration) -6. [Success Criteria](#success-criteria) -7. [Implementation Roadmap](#implementation-roadmap) +**Status:** Draft --- ## Overview -### Purpose -Establish comprehensive test coverage for the Agent365-Python SDK to ensure reliability, maintainability, and quality across all modules. - ### Current State -- ✅ Some unit tests exist for `observability` module -- ⚠️ Partial coverage for `runtime` module +- ✅ Unit tests exist for `observability` and `runtime` modules - ❌ Missing tests for `tooling` and `notifications` modules -- ⚠️ Limited integration test coverage -- ❌ No automated coverage reporting in CI +- ❌ No integration tests or CI/CD automation ### Goals -1. Achieve **80%+ code coverage** across all modules -2. Implement unit tests following Python best practices (`unittest` framework) -3. Create integration tests for cross-module functionality -4. Integrate testing into CI/CD pipeline with automated coverage reporting -5. Establish testing standards for future development +- Achieve **80%+ code coverage** across all modules +- Implement integration tests for cross-module functionality +- Integrate testing into CI/CD pipeline with coverage enforcement --- ## Testing Strategy -### Testing Pyramid - -``` - /\ - / \ Integration Tests - / \ - Module interactions - / \ - Mocked external services - /--------\ - / \ Unit Tests - / \ - Isolated function/class testing ----------------- - 80%+ coverage target -``` - -### Tools & Framework -- **Framework:** `unittest` + `pytest` runner -- **Coverage:** `pytest-cov` -- **Mocking:** `unittest.mock` -- **Async:** `pytest-asyncio` - -### Testing Principles -- **AAA Pattern:** Arrange → Act → Assert -- **FIRST:** Fast, Independent, Repeatable, Self-validating, Timely -- **Naming:** `test___` +**Framework:** `unittest` + `pytest` runner +**Coverage:** `pytest-cov` +**Mocking:** `unittest.mock` +**Async:** `pytest-asyncio` + +**Test Pattern:** AAA (Arrange → Act → Assert) +**Naming Convention:** `test___` + +--- + +## Implementation Roadmap + +| Phase | Deliverables | Priority | +|-------|-------------|----------| +| 1.1 | Runtime unit tests | ✅ Complete | +| 1.2 | Tooling unit tests | HIGH | +| 1.3 | Notifications unit tests | HIGH | +| 1.4 | Expand observability tests | MEDIUM | +| 1.5 | Tooling extension tests | LOW | +| 2 | Integration tests | MEDIUM | +| 3 | CI/CD automation | HIGH | --- ## Phase 1: Unit Tests -### Phase 1.1: Runtime Module +### 1.1 Runtime Module **Priority:** HIGH -| Module | Test File | Status | Priority | -|--------|-----------|--------|----------| -| `power_platform_api_discovery.py` | `test_power_platform_api_discovery.py` | ✅ Exists | Review & Expand | -| `utility.py` | `test_utility.py` | ✅ Exists | Review & Expand | -| `environment_utils.py` | `test_environment_utils.py` | ❌ Missing | **HIGH** | -| `version_utils.py` | `test_version_utils.py` | ❌ Missing | Medium | - -**Key Areas to Test:** -- Environment detection and configuration -- Authentication scope resolution -- Version utilities with deprecation warnings -- Power Platform API discovery +| Module | Test File | Status | +|--------|-----------|--------| +| `power_platform_api_discovery.py` | `test_power_platform_api_discovery.py` | ✅ Complete | +| `utility.py` | `test_utility.py` | ✅ Complete | +| `environment_utils.py` | `test_environment_utils.py` | ✅ Complete | +| `version_utils.py` | `test_version_utils.py` | ✅ Complete | --- -### Phase 1.2: Tooling Module +### 1.2 Tooling Module **Priority:** HIGH -**Directory Structure:** -``` -tests/tooling/ -├── utils/test_utility.py -├── models/test_mcp_server_config.py -└── services/test_mcp_tool_server_configuration_service.py -``` - -**Key Areas to Test:** -- MCP server configuration and validation -- Environment-based URL generation -- Tools mode handling (Mock vs Platform) -- Gateway discovery and authentication -- Manifest file parsing +| Module | Test File | Status | +|--------|-----------|--------| +| `utils/utility.py` | `test_utility.py` | ❌ Missing | +| `models/mcp_server_config.py` | `test_mcp_server_config.py` | ❌ Missing | +| `services/mcp_tool_server_configuration_service.py` | `test_mcp_tool_server_configuration_service.py` | ❌ Missing | --- -### Phase 1.3: Notifications Module +### 1.3 Notifications Module **Priority:** HIGH -**Directory Structure:** -``` -tests/notifications/ -├── models/ -│ ├── test_agent_lifecycle_event.py -│ ├── test_agent_notification_activity.py -│ ├── test_email_reference.py -│ └── test_notification_types.py -└── test_agent_notification.py -``` - -**Key Areas to Test:** -- Activity parsing and entity extraction -- Notification routing and filtering -- Decorator functionality -- Channel and subchannel handling +| Module | Test File | Status | +|--------|-----------|--------| +| `models/agent_lifecycle_event.py` | `test_agent_lifecycle_event.py` | ❌ Missing | +| `models/agent_notification_activity.py` | `test_agent_notification_activity.py` | ❌ Missing | +| `models/email_reference.py` | `test_email_reference.py` | ❌ Missing | +| `agent_notification.py` | `test_agent_notification.py` | ❌ Missing | --- -### Phase 1.4: Observability Extensions +### 1.4 Observability Extensions **Priority:** MEDIUM -| Extension | Action | Priority | -|-----------|--------|----------| -| `agentframework` | Expand existing tests | Medium | -| `langchain` | Expand existing tests | Medium | -| `openai` | Expand existing tests | Medium | -| `semantickernel` | Expand existing tests | Medium | - -**Key Areas to Test:** -- Wrapper functionality -- Trace processing -- Event handling +| Extension | Status | +|-----------|--------| +| `agentframework` | ✅ Expand existing | +| `langchain` | ✅ Expand existing | +| `openai` | ✅ Expand existing | +| `semantickernel` | ✅ Expand existing | --- -### Phase 1.5: Tooling Extensions +### 1.5 Tooling Extensions **Priority:** LOW -**Extensions to Test:** -- Agent Framework tooling integration -- Azure AI Foundry tooling integration -- OpenAI tooling integration -- Semantic Kernel tooling integration +| Extension | Status | +|-----------|--------| +| Agent Framework | ❌ Missing | +| Azure AI Foundry | ❌ Missing | +| OpenAI | ❌ Missing | +| Semantic Kernel | ❌ Missing | --- ## Phase 2: Integration Tests -### Phase 2.1: Module Integration -**Priority:** HIGH - -**Test Scenarios:** -- Runtime + Observability integration -- Tooling + Runtime integration -- Notifications + Runtime integration -- End-to-end workflow scenarios - -**Focus:** -- Cross-module interactions -- Mocked external dependencies -- Configuration propagation -- Error handling across boundaries - ---- - -### Phase 2.2: Extension Integration **Priority:** MEDIUM -**Test Scenarios:** -- Agent Framework full flow -- LangChain full flow -- OpenAI Agents full flow -- Semantic Kernel full flow - -**Focus:** -- End-to-end agent execution with observability -- Tool invocation with MCP servers -- Notification delivery -- Cross-extension compatibility +| Integration | Status | +|-------------|--------| +| Runtime + Observability | ❌ Missing | +| Tooling + Runtime | ❌ Missing | +| Notifications + Runtime | ❌ Missing | +| Agent Framework full flow | ❌ Missing | +| LangChain full flow | ❌ Missing | --- ## Phase 3: CI/CD Integration -### Phase 3.1: Test Automation **Priority:** HIGH -**Setup:** -- GitHub Actions workflow for automated testing -- Multi-version Python matrix (3.9, 3.10, 3.11, 3.12) -- Automated coverage reporting with Codecov -- PR blocking on test failures - ---- - -### Phase 3.2: Coverage Requirements -**Priority:** HIGH - -**Configuration:** -- Minimum 80% code coverage enforcement -- Coverage reports in XML, HTML, and terminal formats -- Branch protection rules requiring passing tests -- Coverage trend tracking - ---- - -### Phase 3.3: Pre-commit Hooks -**Priority:** MEDIUM - -**Setup:** -- Code formatting checks (ruff) -- Test execution before commit -- YAML validation -- Trailing whitespace cleanup +| Component | Status | +|-----------|--------| +| GitHub Actions workflow | ❌ Missing | +| Python matrix (3.9-3.12) | ❌ Missing | +| Coverage enforcement (80%+) | ❌ Missing | +| Codecov integration | ❌ Missing | +| PR blocking on failures | ❌ Missing | --- ## Success Criteria -### Phase 1: Unit Tests - ✅ 80%+ code coverage for all modules -- ✅ All tests follow AAA pattern -- ✅ Tests run independently in any order -- ✅ Full suite completes in < 30 seconds - -### Phase 2: Integration Tests -- ✅ All major integration points tested -- ✅ 99%+ test reliability -- ✅ External services properly mocked -- ✅ Integration scenarios documented - -### Phase 3: CI/CD +- ✅ All tests pass independently +- ✅ Full suite completes in < 30 seconds (unit) / < 5 minutes (full) - ✅ Automated test execution on all PRs - ✅ Coverage reports visible and enforced -- ✅ PR merge blocked on test failures or coverage drops -- ✅ Tests pass on Python 3.9-3.12 -- ✅ Full suite completes in < 5 minutes - ---- - -## Implementation Roadmap - -| Phase | Focus | Deliverables | Owner | -|-------|-------|-------------|-------| -| 1.1 | Runtime Module | `test_environment_utils.py`, `test_version_utils.py` | TBD | -| 1.2 | Tooling Module | All tooling test files | TBD | -| 1.3 | Notifications Module | All notifications test files | TBD | -| 1.4 | Observability Extensions | Expand existing tests | TBD | -| 1.5 | Tooling Extensions | Extension test files | TBD | -| 2.1 | Module Integration | Integration test suite | TBD | -| 2.2 | Extension Integration | Full flow tests | TBD | -| 3 | CI/CD Setup | GitHub Actions, coverage, pre-commit | TBD | - -### Key Milestones -- **M1:** Runtime tests complete, baseline coverage established -- **M2:** All core module unit tests complete -- **M3:** Integration test framework in place -- **M4:** Full CI/CD integration, coverage enforcement - ---- - -## References - -- [Python unittest Documentation](https://docs.python.org/3/library/unittest.html) -- [pytest Documentation](https://docs.pytest.org/) -- [unittest.mock Guide](https://docs.python.org/3/library/unittest.mock.html) -- Existing tests: `tests/observability/core/` and `tests/runtime/` From d949253c0146e2c40b8ca7681fbf9ae6ca9a33c1 Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 17:17:15 -0800 Subject: [PATCH 07/14] Add min tests --- tests/runtime/test_environment_utils.py | 103 ++---------------------- tests/runtime/test_version_utils.py | 102 +---------------------- 2 files changed, 10 insertions(+), 195 deletions(-) diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index 8710ae99..b31779c4 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -5,7 +5,6 @@ import os import unittest -from unittest.mock import patch from microsoft_agents_a365.runtime.environment_utils import ( DEVELOPMENT_ENVIRONMENT_NAME, @@ -18,14 +17,15 @@ class TestEnvironmentUtils(unittest.TestCase): - """Test cases for environment utility functions.""" + """Test cases for environment utility functions. + + Tests: Constants validation, observability scope retrieval, and environment detection logic. + """ def tearDown(self): """Clean up environment variables after each test.""" - env_vars = ["PYTHON_ENVIRONMENT"] - for var in env_vars: - if var in os.environ: - del os.environ[var] + if "PYTHON_ENVIRONMENT" in os.environ: + del os.environ["PYTHON_ENVIRONMENT"] def test_constants(self): """Test that environment constants have expected values.""" @@ -44,7 +44,6 @@ def test_get_observability_authentication_scope_returns_prod_scope(self): def test_is_development_environment_with_no_env_var(self): """Test is_development_environment returns False when no environment variable is set.""" - # Ensure environment variables are not set if "PYTHON_ENVIRONMENT" in os.environ: del os.environ["PYTHON_ENVIRONMENT"] @@ -60,16 +59,6 @@ def test_is_development_environment_with_development_env_var(self): self.assertTrue(result) - def test_is_development_environment_case_insensitive(self): - """Test is_development_environment is case-insensitive.""" - test_cases = ["development", "DEVELOPMENT", "DeveLoPMenT", "Development"] - - for env_value in test_cases: - with self.subTest(env_value=env_value): - os.environ["PYTHON_ENVIRONMENT"] = env_value - result = is_development_environment() - self.assertTrue(result) - def test_is_development_environment_with_production_env_var(self): """Test is_development_environment returns False when PYTHON_ENVIRONMENT is 'production'.""" os.environ["PYTHON_ENVIRONMENT"] = "production" @@ -78,86 +67,6 @@ def test_is_development_environment_with_production_env_var(self): self.assertFalse(result) - def test_is_development_environment_with_other_env_var(self): - """Test is_development_environment returns False for other environment values.""" - test_cases = ["staging", "test", "preprod", "custom"] - - for env_value in test_cases: - with self.subTest(env_value=env_value): - os.environ["PYTHON_ENVIRONMENT"] = env_value - result = is_development_environment() - self.assertFalse(result) - - def test_is_development_environment_with_empty_env_var(self): - """Test is_development_environment returns False when PYTHON_ENVIRONMENT is empty.""" - os.environ["PYTHON_ENVIRONMENT"] = "" - - result = is_development_environment() - - self.assertFalse(result) - - def test_is_development_environment_with_whitespace_env_var(self): - """Test is_development_environment returns False when PYTHON_ENVIRONMENT is whitespace.""" - os.environ["PYTHON_ENVIRONMENT"] = " " - - result = is_development_environment() - - self.assertFalse(result) - - @patch.dict(os.environ, {"PYTHON_ENVIRONMENT": "Development"}, clear=False) - def test_python_environment_precedence(self): - """Test that PYTHON_ENVIRONMENT takes precedence.""" - result = is_development_environment() - - self.assertTrue(result) - - def test_default_environment_is_production(self): - """Test that the default environment is production when no env vars are set.""" - # Ensure no environment variables are set - if "PYTHON_ENVIRONMENT" in os.environ: - del os.environ["PYTHON_ENVIRONMENT"] - - # The _get_current_environment function should default to PRODUCTION_ENVIRONMENT_NAME - result = is_development_environment() - - self.assertFalse(result) - - -class TestObservabilityAuthenticationScope(unittest.TestCase): - """Test cases for observability authentication scope.""" - - def test_scope_is_list(self): - """Test that the scope is returned as a list.""" - result = get_observability_authentication_scope() - - self.assertIsInstance(result, list) - - def test_scope_contains_single_value(self): - """Test that the scope list contains exactly one value.""" - result = get_observability_authentication_scope() - - self.assertEqual(len(result), 1) - - def test_scope_value_is_string(self): - """Test that the scope value is a string.""" - result = get_observability_authentication_scope() - - self.assertIsInstance(result[0], str) - - def test_scope_value_format(self): - """Test that the scope value has the correct format.""" - result = get_observability_authentication_scope() - - self.assertTrue(result[0].startswith("https://")) - self.assertTrue(result[0].endswith(".default")) - - def test_scope_consistency(self): - """Test that multiple calls return the same scope.""" - result1 = get_observability_authentication_scope() - result2 = get_observability_authentication_scope() - - self.assertEqual(result1, result2) - if __name__ == "__main__": unittest.main() diff --git a/tests/runtime/test_version_utils.py b/tests/runtime/test_version_utils.py index dd5a75e8..f5b30591 100644 --- a/tests/runtime/test_version_utils.py +++ b/tests/runtime/test_version_utils.py @@ -11,24 +11,18 @@ class TestVersionUtils(unittest.TestCase): - """Test cases for version utility functions.""" + """Test cases for version utility functions. + + Tests: Default version behavior, environment variable usage, and deprecation warning. + """ def tearDown(self): """Clean up environment variables after each test.""" if "AGENT365_PYTHON_SDK_PACKAGE_VERSION" in os.environ: del os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] - def test_build_version_returns_string(self): - """Test that build_version returns a string.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - result = build_version() - - self.assertIsInstance(result, str) - def test_build_version_default_value(self): """Test build_version returns default version when no env var is set.""" - # Ensure environment variable is not set if "AGENT365_PYTHON_SDK_PACKAGE_VERSION" in os.environ: del os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] @@ -49,36 +43,6 @@ def test_build_version_with_env_var(self): self.assertEqual(result, test_version) - def test_build_version_with_complex_version(self): - """Test build_version with complex version strings.""" - test_cases = [ - "1.0.0-alpha", - "2.3.4-beta.1", - "3.0.0-rc.2", - "1.2.3+build.456", - "2.0.0-alpha.1+build.789", - ] - - for version in test_cases: - with self.subTest(version=version): - os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = version - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - result = build_version() - - self.assertEqual(result, version) - - def test_build_version_with_empty_env_var(self): - """Test build_version with empty environment variable.""" - os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = "" - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - result = build_version() - - self.assertEqual(result, "") - def test_build_version_deprecation_warning(self): """Test that build_version raises DeprecationWarning.""" with self.assertWarns(DeprecationWarning) as cm: @@ -88,64 +52,6 @@ def test_build_version_deprecation_warning(self): self.assertIn("deprecated", warning_message.lower()) self.assertIn("setuptools-git-versioning", warning_message) - def test_build_version_deprecation_warning_message(self): - """Test that deprecation warning contains the correct message.""" - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - build_version() - - # Check that exactly one warning was raised - self.assertEqual(len(w), 1) - - # Check that it's a DeprecationWarning - self.assertTrue(issubclass(w[0].category, DeprecationWarning)) - - # Check the message content - message = str(w[0].message) - self.assertIn("build_version() is deprecated", message) - self.assertIn("setuptools-git-versioning", message) - - def test_build_version_stacklevel(self): - """Test that deprecation warning has correct stack level.""" - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - build_version() - - # The warning should point to the caller of build_version - # not to the function itself (stacklevel=2) - self.assertEqual(len(w), 1) - warning = w[0] - - # Verify the warning was captured - self.assertIsNotNone(warning.filename) - self.assertGreater(warning.lineno, 0) - - -class TestVersionUtilsDocstring(unittest.TestCase): - """Test cases for version_utils module documentation.""" - - def test_module_has_docstring(self): - """Test that the module has a docstring.""" - import microsoft_agents_a365.runtime.version_utils as version_utils - - self.assertIsNotNone(version_utils.__doc__) - self.assertGreater(len(version_utils.__doc__), 0) - - def test_module_docstring_mentions_deprecation(self): - """Test that module docstring mentions deprecation.""" - import microsoft_agents_a365.runtime.version_utils as version_utils - - self.assertIn("deprecated", version_utils.__doc__.lower()) - - def test_function_has_docstring(self): - """Test that build_version function has a docstring.""" - self.assertIsNotNone(build_version.__doc__) - self.assertGreater(len(build_version.__doc__), 0) - - def test_function_docstring_mentions_deprecation(self): - """Test that function docstring mentions deprecation.""" - self.assertIn("DEPRECATED", build_version.__doc__) - if __name__ == "__main__": unittest.main() From d0db36b82adacfb47f5f48b99dd6f6fd3d15a9ff Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Mon, 24 Nov 2025 17:18:33 -0800 Subject: [PATCH 08/14] ruff check --- tests/runtime/test_environment_utils.py | 2 +- tests/runtime/test_version_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index b31779c4..b064f7d5 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -18,7 +18,7 @@ class TestEnvironmentUtils(unittest.TestCase): """Test cases for environment utility functions. - + Tests: Constants validation, observability scope retrieval, and environment detection logic. """ diff --git a/tests/runtime/test_version_utils.py b/tests/runtime/test_version_utils.py index f5b30591..21691292 100644 --- a/tests/runtime/test_version_utils.py +++ b/tests/runtime/test_version_utils.py @@ -12,7 +12,7 @@ class TestVersionUtils(unittest.TestCase): """Test cases for version utility functions. - + Tests: Default version behavior, environment variable usage, and deprecation warning. """ From 81c792e0a9d5176bea71bf601f1d78b77c45f554 Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Tue, 25 Nov 2025 11:41:50 -0800 Subject: [PATCH 09/14] Fix test cases --- tests/runtime/test_environment_utils.py | 82 +++----- tests/runtime/test_utility.py | 267 +++++++++++------------- 2 files changed, 143 insertions(+), 206 deletions(-) diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index b064f7d5..77d8c803 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -4,69 +4,39 @@ """Unit tests for environment_utils module.""" import os -import unittest +import pytest from microsoft_agents_a365.runtime.environment_utils import ( - DEVELOPMENT_ENVIRONMENT_NAME, - PROD_OBSERVABILITY_CLUSTER_CATEGORY, PROD_OBSERVABILITY_SCOPE, - PRODUCTION_ENVIRONMENT_NAME, get_observability_authentication_scope, is_development_environment, ) -class TestEnvironmentUtils(unittest.TestCase): - """Test cases for environment utility functions. +def test_get_observability_authentication_scope(): + """Test get_observability_authentication_scope returns production scope.""" + result = get_observability_authentication_scope() + assert result == [PROD_OBSERVABILITY_SCOPE] - Tests: Constants validation, observability scope retrieval, and environment detection logic. - """ - def tearDown(self): - """Clean up environment variables after each test.""" - if "PYTHON_ENVIRONMENT" in os.environ: - del os.environ["PYTHON_ENVIRONMENT"] - - def test_constants(self): - """Test that environment constants have expected values.""" - self.assertEqual(PROD_OBSERVABILITY_SCOPE, "https://api.powerplatform.com/.default") - self.assertEqual(PROD_OBSERVABILITY_CLUSTER_CATEGORY, "prod") - self.assertEqual(PRODUCTION_ENVIRONMENT_NAME, "production") - self.assertEqual(DEVELOPMENT_ENVIRONMENT_NAME, "Development") - - def test_get_observability_authentication_scope_returns_prod_scope(self): - """Test that get_observability_authentication_scope returns production scope.""" - result = get_observability_authentication_scope() - - self.assertIsInstance(result, list) - self.assertEqual(len(result), 1) - self.assertEqual(result[0], PROD_OBSERVABILITY_SCOPE) - - def test_is_development_environment_with_no_env_var(self): - """Test is_development_environment returns False when no environment variable is set.""" - if "PYTHON_ENVIRONMENT" in os.environ: - del os.environ["PYTHON_ENVIRONMENT"] - - result = is_development_environment() - - self.assertFalse(result) - - def test_is_development_environment_with_development_env_var(self): - """Test is_development_environment returns True when PYTHON_ENVIRONMENT is 'Development'.""" - os.environ["PYTHON_ENVIRONMENT"] = "Development" - - result = is_development_environment() - - self.assertTrue(result) - - def test_is_development_environment_with_production_env_var(self): - """Test is_development_environment returns False when PYTHON_ENVIRONMENT is 'production'.""" - os.environ["PYTHON_ENVIRONMENT"] = "production" - - result = is_development_environment() - - self.assertFalse(result) - - -if __name__ == "__main__": - unittest.main() +@pytest.mark.parametrize( + "env_value,expected", + [ + (None, False), + ("Development", True), + ("production", False), + ("staging", False), + ], +) +def test_is_development_environment(env_value, expected): + """Test is_development_environment returns correct value based on PYTHON_ENVIRONMENT.""" + if env_value is None: + os.environ.pop("PYTHON_ENVIRONMENT", None) + else: + os.environ["PYTHON_ENVIRONMENT"] = env_value + + result = is_development_environment() + assert result == expected + + # Cleanup + os.environ.pop("PYTHON_ENVIRONMENT", None) diff --git a/tests/runtime/test_utility.py b/tests/runtime/test_utility.py index d6cd2d4f..2ce17e2e 100644 --- a/tests/runtime/test_utility.py +++ b/tests/runtime/test_utility.py @@ -1,159 +1,126 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Unit tests for Utility class.""" -import unittest import uuid -import jwt +from unittest.mock import Mock +import jwt +import pytest from microsoft_agents_a365.runtime.utility import Utility -class TestUtility(unittest.TestCase): - """Test cases for the Utility class.""" +# Fixtures (Mocks and Helpers) +@pytest.fixture +def create_test_jwt(): + """Fixture to create test JWT tokens.""" - def setUp(self): - """Set up test fixtures.""" - self.test_app_id = "12345678-1234-1234-1234-123456789abc" - self.test_azp_id = "87654321-4321-4321-4321-cba987654321" - - def create_test_jwt(self, claims: dict) -> str: - """Create a test JWT token with the given claims.""" - # Use PyJWT to create a proper JWT token (unsigned for testing) + def _create(claims: dict) -> str: return jwt.encode(claims, key="", algorithm="none") - def test_get_app_id_from_token_with_none_token(self): - """Test get_app_id_from_token with None token.""" - result = Utility.get_app_id_from_token(None) - self.assertEqual(result, str(uuid.UUID(int=0))) - - def test_get_app_id_from_token_with_empty_token(self): - """Test get_app_id_from_token with empty token.""" - result = Utility.get_app_id_from_token("") - self.assertEqual(result, str(uuid.UUID(int=0))) - - result = Utility.get_app_id_from_token(" ") - self.assertEqual(result, str(uuid.UUID(int=0))) - - def test_get_app_id_from_token_with_appid_claim(self): - """Test get_app_id_from_token with appid claim.""" - token = self.create_test_jwt({"appid": self.test_app_id, "other": "value"}) - result = Utility.get_app_id_from_token(token) - self.assertEqual(result, self.test_app_id) - - def test_get_app_id_from_token_with_azp_claim(self): - """Test get_app_id_from_token with azp claim.""" - token = self.create_test_jwt({"azp": self.test_azp_id, "other": "value"}) - result = Utility.get_app_id_from_token(token) - self.assertEqual(result, self.test_azp_id) - - def test_get_app_id_from_token_with_both_claims(self): - """Test get_app_id_from_token with both appid and azp claims (appid takes precedence).""" - token = self.create_test_jwt({"appid": self.test_app_id, "azp": self.test_azp_id}) - result = Utility.get_app_id_from_token(token) - self.assertEqual(result, self.test_app_id) - - def test_get_app_id_from_token_without_app_claims(self): - """Test get_app_id_from_token with token containing no app claims.""" - token = self.create_test_jwt({"sub": "user123", "iss": "issuer"}) - result = Utility.get_app_id_from_token(token) - self.assertEqual(result, "") - - def test_get_app_id_from_token_with_invalid_token(self): - """Test get_app_id_from_token with invalid token formats.""" - # Invalid token format - result = Utility.get_app_id_from_token("invalid.token") - self.assertEqual(result, "") - - # Token with only two parts - result = Utility.get_app_id_from_token("header.payload") - self.assertEqual(result, "") - - # Token with invalid base64 - result = Utility.get_app_id_from_token("invalid.!!!invalid!!!.signature") - self.assertEqual(result, "") - - -class MockActivity: - """Mock activity class for testing.""" - - def __init__(self, is_agentic: bool = False, agentic_id: str = ""): - self._is_agentic = is_agentic - self._agentic_id = agentic_id - - def is_agentic_request(self) -> bool: - return self._is_agentic - - def get_agentic_instance_id(self) -> str: - return self._agentic_id - - -class MockContext: - """Mock context class for testing.""" - - def __init__(self, activity=None): - self.activity = activity - - -class TestUtilityResolveAgentIdentity(unittest.TestCase): - """Test cases for the resolve_agent_identity method.""" - - def setUp(self): - """Set up test fixtures.""" - self.test_app_id = "token-app-id-123" - self.agentic_id = "agentic-id-456" - - # Create a test token with PyJWT - claims = {"appid": self.test_app_id} - self.test_token = jwt.encode(claims, key="", algorithm="none") - - def test_resolve_agent_identity_with_agentic_request(self): - """Test resolve_agent_identity with agentic request.""" - activity = MockActivity(is_agentic=True, agentic_id=self.agentic_id) - context = MockContext(activity) - - result = Utility.resolve_agent_identity(context, self.test_token) - self.assertEqual(result, self.agentic_id) - - def test_resolve_agent_identity_with_non_agentic_request(self): - """Test resolve_agent_identity with non-agentic request.""" - activity = MockActivity(is_agentic=False) - context = MockContext(activity) - - result = Utility.resolve_agent_identity(context, self.test_token) - self.assertEqual(result, self.test_app_id) - - def test_resolve_agent_identity_with_context_without_activity(self): - """Test resolve_agent_identity with context that has no activity.""" - context = MockContext() - - result = Utility.resolve_agent_identity(context, self.test_token) - self.assertEqual(result, self.test_app_id) - - def test_resolve_agent_identity_with_none_context(self): - """Test resolve_agent_identity with None context.""" - result = Utility.resolve_agent_identity(None, self.test_token) - self.assertEqual(result, self.test_app_id) - - def test_resolve_agent_identity_with_agentic_but_empty_id(self): - """Test resolve_agent_identity with agentic request but empty agentic ID.""" - activity = MockActivity(is_agentic=True, agentic_id="") - context = MockContext(activity) - - result = Utility.resolve_agent_identity(context, self.test_token) - self.assertEqual(result, "") - - def test_resolve_agent_identity_fallback_on_exception(self): - """Test resolve_agent_identity falls back to token when context access fails.""" - - # Create a context that will raise an exception when accessed - class FaultyContext: - @property - def activity(self): - raise RuntimeError("Context access failed") - - context = FaultyContext() - result = Utility.resolve_agent_identity(context, self.test_token) - self.assertEqual(result, self.test_app_id) - - -if __name__ == "__main__": - unittest.main() + return _create + + +@pytest.fixture +def mock_activity(): + """Fixture to create mock activity.""" + + def _create(is_agentic=False, agentic_id=""): + activity = Mock() + activity.is_agentic_request.return_value = is_agentic + activity.get_agentic_instance_id.return_value = agentic_id + return activity + + return _create + + +@pytest.fixture +def mock_context(): + """Fixture to create mock context.""" + + def _create(activity=None): + context = Mock() + context.activity = activity + return context + + return _create + + +# Tests for get_app_id_from_token +@pytest.mark.parametrize( + "token,expected", + [ + (None, str(uuid.UUID(int=0))), + ("", str(uuid.UUID(int=0))), + (" ", str(uuid.UUID(int=0))), + ("invalid.token", ""), + ], +) +def test_get_app_id_from_token_invalid(token, expected): + """Test get_app_id_from_token handles invalid tokens correctly.""" + result = Utility.get_app_id_from_token(token) + assert result == expected + + +@pytest.mark.parametrize( + "claims,expected", + [ + ({"appid": "test-app-id"}, "test-app-id"), + ({"azp": "azp-app-id"}, "azp-app-id"), + ({"appid": "appid-value", "azp": "azp-value"}, "appid-value"), + ({"sub": "user123"}, ""), + ], +) +def test_get_app_id_from_token_valid_tokens(create_test_jwt, claims, expected): + """Test get_app_id_from_token with valid tokens and various claims.""" + token = create_test_jwt(claims) + result = Utility.get_app_id_from_token(token) + assert result == expected + + +# Tests for resolve_agent_identity +@pytest.mark.parametrize( + "is_agentic,agentic_id,expected", + [ + (True, "agentic-id", "agentic-id"), + (True, "", ""), + (False, "", "token-app-id"), + (False, "ignored-id", "token-app-id"), + ], +) +def test_resolve_agent_identity_with_context( + create_test_jwt, mock_activity, mock_context, is_agentic, agentic_id, expected +): + """Test resolve_agent_identity returns correct ID based on context.""" + token = create_test_jwt({"appid": "token-app-id"}) + activity = mock_activity(is_agentic=is_agentic, agentic_id=agentic_id) + context = mock_context(activity) + + result = Utility.resolve_agent_identity(context, token) + assert result == expected + + +@pytest.mark.parametrize( + "context", + [ + None, + Mock(activity=None), + ], +) +def test_resolve_agent_identity_fallback(create_test_jwt, context): + """Test resolve_agent_identity falls back to token when context is invalid.""" + token = create_test_jwt({"appid": "token-app-id"}) + result = Utility.resolve_agent_identity(context, token) + assert result == "token-app-id" + + +def test_resolve_agent_identity_exception_handling(create_test_jwt, mock_context): + """Test resolve_agent_identity falls back to token when activity methods raise exceptions.""" + token = create_test_jwt({"appid": "token-app-id"}) + activity = Mock() + activity.is_agentic_request.side_effect = AttributeError("Method not available") + context = mock_context(activity) + + result = Utility.resolve_agent_identity(context, token) + assert result == "token-app-id" From 9c046c3e47c0a3bc0a0b87385dc4e63fd784644c Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Tue, 25 Nov 2025 11:49:54 -0800 Subject: [PATCH 10/14] fix tests --- tests/runtime/test_version_utils.py | 78 ++++++++++++----------------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/tests/runtime/test_version_utils.py b/tests/runtime/test_version_utils.py index 21691292..5da6851b 100644 --- a/tests/runtime/test_version_utils.py +++ b/tests/runtime/test_version_utils.py @@ -3,55 +3,39 @@ """Unit tests for version_utils module.""" -import os -import unittest import warnings +import pytest from microsoft_agents_a365.runtime.version_utils import build_version -class TestVersionUtils(unittest.TestCase): - """Test cases for version utility functions. - - Tests: Default version behavior, environment variable usage, and deprecation warning. - """ - - def tearDown(self): - """Clean up environment variables after each test.""" - if "AGENT365_PYTHON_SDK_PACKAGE_VERSION" in os.environ: - del os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] - - def test_build_version_default_value(self): - """Test build_version returns default version when no env var is set.""" - if "AGENT365_PYTHON_SDK_PACKAGE_VERSION" in os.environ: - del os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - result = build_version() - - self.assertEqual(result, "0.0.0") - - def test_build_version_with_env_var(self): - """Test build_version returns version from environment variable.""" - test_version = "1.2.3" - os.environ["AGENT365_PYTHON_SDK_PACKAGE_VERSION"] = test_version - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - result = build_version() - - self.assertEqual(result, test_version) - - def test_build_version_deprecation_warning(self): - """Test that build_version raises DeprecationWarning.""" - with self.assertWarns(DeprecationWarning) as cm: - build_version() - - warning_message = str(cm.warning) - self.assertIn("deprecated", warning_message.lower()) - self.assertIn("setuptools-git-versioning", warning_message) - - -if __name__ == "__main__": - unittest.main() +# Tests for build_version +@pytest.mark.parametrize( + "env_value,expected", + [ + (None, "0.0.0"), + ("1.2.3", "1.2.3"), + ("2.5.0-beta", "2.5.0-beta"), + ("", ""), + ], +) +def test_build_version(monkeypatch, env_value, expected): + """Test build_version returns correct version based on environment variable.""" + if env_value is None: + monkeypatch.delenv("AGENT365_PYTHON_SDK_PACKAGE_VERSION", raising=False) + else: + monkeypatch.setenv("AGENT365_PYTHON_SDK_PACKAGE_VERSION", env_value) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = build_version() + + assert result == expected + + +def test_build_version_deprecation_warning(): + """Test that build_version raises DeprecationWarning with correct message.""" + with pytest.warns( + DeprecationWarning, match="build_version.*deprecated.*setuptools-git-versioning" + ): + build_version() From d338dc8797d9706ca7ed9e605e0f215fc833da02 Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Tue, 25 Nov 2025 12:03:37 -0800 Subject: [PATCH 11/14] fix --- .../test_power_platform_api_discovery.py | 326 +++++++++--------- 1 file changed, 167 insertions(+), 159 deletions(-) diff --git a/tests/runtime/test_power_platform_api_discovery.py b/tests/runtime/test_power_platform_api_discovery.py index 2a16c66c..0d7f3641 100644 --- a/tests/runtime/test_power_platform_api_discovery.py +++ b/tests/runtime/test_power_platform_api_discovery.py @@ -1,159 +1,167 @@ -# Copyright (c) Microsoft. All rights reserved. - -import unittest - -from microsoft_agents_a365.runtime.power_platform_api_discovery import PowerPlatformApiDiscovery - - -class TestPowerPlatformApiDiscovery(unittest.TestCase): - def test_host_suffix_and_audience(self): - expected_host_suffixes = { - "local": "api.powerplatform.localhost", - "dev": "api.powerplatform.com", - "test": "api.powerplatform.com", - "preprod": "api.powerplatform.com", - "firstrelease": "api.powerplatform.com", - "prod": "api.powerplatform.com", - "gov": "api.gov.powerplatform.microsoft.us", - "high": "api.high.powerplatform.microsoft.us", - "dod": "api.appsplatform.us", - "mooncake": "api.powerplatform.partner.microsoftonline.cn", - "ex": "api.powerplatform.eaglex.ic.gov", - "rx": "api.powerplatform.microsoft.scloud", - } - - for cluster, expected in expected_host_suffixes.items(): - with self.subTest(cluster=cluster): - disc = PowerPlatformApiDiscovery(cluster) # type: ignore[arg-type] - self.assertEqual(disc.get_token_endpoint_host(), expected) - self.assertEqual(disc.get_token_audience(), f"https://{expected}") - - def test_hex_suffix_length_rules(self): - prod = PowerPlatformApiDiscovery("prod") - first = PowerPlatformApiDiscovery("firstrelease") - dev = PowerPlatformApiDiscovery("dev") - - self.assertEqual(prod._get_hex_api_suffix_length(), 2) - self.assertEqual(first._get_hex_api_suffix_length(), 2) - self.assertEqual(dev._get_hex_api_suffix_length(), 1) - - def test_tenant_endpoint_generation_prod(self): - disc = PowerPlatformApiDiscovery("prod") - tenant_id = "abc-012" # normalized -> abc012; suffix length 2 -> '12' - expected = "abc0.12.tenant.api.powerplatform.com" - self.assertEqual(disc.get_tenant_endpoint(tenant_id), expected) - - def test_tenant_endpoint_generation_dev(self): - disc = PowerPlatformApiDiscovery("dev") - tenant_id = "A1B2" # normalized -> a1b2; suffix length 1 -> '2' - expected = "a1b.2.tenant.api.powerplatform.com" - self.assertEqual(disc.get_tenant_endpoint(tenant_id), expected) - - def test_tenant_island_cluster_endpoint(self): - disc = PowerPlatformApiDiscovery("prod") - tenant_id = "abc-1234" # normalized -> abc1234; suffix '34', prefix 'abc12' - expected = "il-abc12.34.tenant.api.powerplatform.com" - self.assertEqual(disc.get_tenant_island_cluster_endpoint(tenant_id), expected) - - def test_invalid_characters_in_tenant_identifier(self): - disc = PowerPlatformApiDiscovery("dev") - with self.assertRaisesRegex(ValueError, r"invalid host name characters"): - disc.get_tenant_endpoint("invalid$name") - - def test_tenant_identifier_too_short_for_suffix(self): - disc = PowerPlatformApiDiscovery("prod") - # prod requires normalized length >= 3 (2 + 1). Provide only 2 characters. - with self.assertRaisesRegex(ValueError, r"must be at least"): - disc.get_tenant_endpoint("ab") - - def test_normalization_of_tenant_id(self): - disc = PowerPlatformApiDiscovery("dev") - tenant_id = "Ab-Cd-Ef" # normalized -> abcdef; suffix 1 -> 'f', prefix 'abcde' - expected = "abcde.f.tenant.api.powerplatform.com" - self.assertEqual(disc.get_tenant_endpoint(tenant_id), expected) - - def test_nodejs_tenant_examples(self): - tenant_id = "e3064512-cc6d-4703-be71-a2ecaecaa98a" - expected_map = { - "local": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.localhost", - "dev": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com", - "test": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com", - "preprod": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com", - "firstrelease": "e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com", - "prod": "e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com", - "gov": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.gov.powerplatform.microsoft.us", - "high": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.high.powerplatform.microsoft.us", - "dod": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.appsplatform.us", - "mooncake": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.partner.microsoftonline.cn", - "ex": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.eaglex.ic.gov", - "rx": "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.microsoft.scloud", - } - - for cluster, expected in expected_map.items(): - with self.subTest(cluster=cluster): - disc = PowerPlatformApiDiscovery(cluster) # type: ignore[arg-type] - self.assertEqual(disc.get_tenant_endpoint(tenant_id), expected) - - def test_nodejs_tenant_island_examples(self): - tenant_id = "e3064512-cc6d-4703-be71-a2ecaecaa98a" - expected_map = { - "local": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.localhost", - "dev": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com", - "test": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com", - "preprod": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com", - "firstrelease": "il-e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com", - "prod": "il-e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com", - "gov": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.gov.powerplatform.microsoft.us", - "high": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.high.powerplatform.microsoft.us", - "dod": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.appsplatform.us", - "mooncake": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.partner.microsoftonline.cn", - "ex": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.eaglex.ic.gov", - "rx": "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.microsoft.scloud", - } - - for cluster, expected in expected_map.items(): - with self.subTest(cluster=cluster): - disc = PowerPlatformApiDiscovery(cluster) # type: ignore[arg-type] - self.assertEqual(disc.get_tenant_island_cluster_endpoint(tenant_id), expected) - - def test_nodejs_invalid_characters_exact_message(self): - disc = PowerPlatformApiDiscovery("local") - expected_msg = "Cannot generate Power Platform API endpoint because the tenant identifier contains invalid host name characters, only alphanumeric and dash characters are expected: invalid?" - with self.assertRaises(ValueError) as cm: - disc.get_tenant_endpoint("invalid?") - self.assertEqual(str(cm.exception), expected_msg) - - def test_nodejs_insufficient_length_exact_messages(self): - disc_local = PowerPlatformApiDiscovery("local") - with self.assertRaises(ValueError) as cm1: - disc_local.get_tenant_endpoint("a") - self.assertEqual( - str(cm1.exception), - "Cannot generate Power Platform API endpoint because the normalized tenant identifier must be at least 2 characters in length: a", - ) - - with self.assertRaises(ValueError) as cm2: - disc_local.get_tenant_endpoint("a-") - self.assertEqual( - str(cm2.exception), - "Cannot generate Power Platform API endpoint because the normalized tenant identifier must be at least 2 characters in length: a", - ) - - disc_prod = PowerPlatformApiDiscovery("prod") - with self.assertRaises(ValueError) as cm3: - disc_prod.get_tenant_endpoint("aa") - self.assertEqual( - str(cm3.exception), - "Cannot generate Power Platform API endpoint because the normalized tenant identifier must be at least 3 characters in length: aa", - ) - - with self.assertRaises(ValueError) as cm4: - disc_prod.get_tenant_endpoint("a-a") - self.assertEqual( - str(cm4.exception), - "Cannot generate Power Platform API endpoint because the normalized tenant identifier must be at least 3 characters in length: aa", - ) - - -if __name__ == "__main__": - unittest.main() +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Unit tests for PowerPlatformApiDiscovery class.""" + +import re +import pytest + +from microsoft_agents_a365.runtime.power_platform_api_discovery import ( + PowerPlatformApiDiscovery, +) + + +# Tests for get_token_endpoint_host and get_token_audience +@pytest.mark.parametrize( + "cluster,expected_host", + [ + ("local", "api.powerplatform.localhost"), + ("dev", "api.powerplatform.com"), + ("test", "api.powerplatform.com"), + ("preprod", "api.powerplatform.com"), + ("firstrelease", "api.powerplatform.com"), + ("prod", "api.powerplatform.com"), + ("gov", "api.gov.powerplatform.microsoft.us"), + ("high", "api.high.powerplatform.microsoft.us"), + ("dod", "api.appsplatform.us"), + ("mooncake", "api.powerplatform.partner.microsoftonline.cn"), + ("ex", "api.powerplatform.eaglex.ic.gov"), + ("rx", "api.powerplatform.microsoft.scloud"), + ], +) +def test_host_suffix_and_audience(cluster, expected_host): + """Test get_token_endpoint_host and get_token_audience return correct values for each cluster.""" + disc = PowerPlatformApiDiscovery(cluster) + assert disc.get_token_endpoint_host() == expected_host + assert disc.get_token_audience() == f"https://{expected_host}" + + +# Tests for _get_hex_api_suffix_length +@pytest.mark.parametrize( + "cluster,expected_length", + [ + ("prod", 2), + ("firstrelease", 2), + ("dev", 1), + ("test", 1), + ("preprod", 1), + ("local", 1), + ("gov", 1), + ("high", 1), + ("dod", 1), + ("mooncake", 1), + ("ex", 1), + ("rx", 1), + ], +) +def test_hex_suffix_length_rules(cluster, expected_length): + """Test _get_hex_api_suffix_length returns correct suffix length for each cluster.""" + disc = PowerPlatformApiDiscovery(cluster) + assert disc._get_hex_api_suffix_length() == expected_length + + +# Tests for get_tenant_endpoint +@pytest.mark.parametrize( + "cluster,tenant_id,expected", + [ + ("prod", "abc-012", "abc0.12.tenant.api.powerplatform.com"), + ("dev", "A1B2", "a1b.2.tenant.api.powerplatform.com"), + ("dev", "Ab-Cd-Ef", "abcde.f.tenant.api.powerplatform.com"), + ], +) +def test_tenant_endpoint_generation(cluster, tenant_id, expected): + """Test get_tenant_endpoint for various clusters and tenant IDs.""" + disc = PowerPlatformApiDiscovery(cluster) + assert disc.get_tenant_endpoint(tenant_id) == expected + + +@pytest.mark.parametrize( + "cluster,expected", + [ + ("local", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.localhost"), + ("dev", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com"), + ("test", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com"), + ("preprod", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com"), + ("firstrelease", "e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com"), + ("prod", "e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com"), + ("gov", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.gov.powerplatform.microsoft.us"), + ("high", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.high.powerplatform.microsoft.us"), + ("dod", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.appsplatform.us"), + ( + "mooncake", + "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.partner.microsoftonline.cn", + ), + ("ex", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.eaglex.ic.gov"), + ("rx", "e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.microsoft.scloud"), + ], +) +def test_tenant_endpoint_with_real_uuid(cluster, expected): + """Test get_tenant_endpoint with real UUID across all clusters.""" + tenant_id = "e3064512-cc6d-4703-be71-a2ecaecaa98a" + disc = PowerPlatformApiDiscovery(cluster) + assert disc.get_tenant_endpoint(tenant_id) == expected + + +# Tests for get_tenant_island_cluster_endpoint +@pytest.mark.parametrize( + "cluster,expected", + [ + ("local", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.localhost"), + ("dev", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com"), + ("test", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com"), + ("preprod", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.com"), + ("firstrelease", "il-e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com"), + ("prod", "il-e3064512cc6d4703be71a2ecaecaa9.8a.tenant.api.powerplatform.com"), + ("gov", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.gov.powerplatform.microsoft.us"), + ("high", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.high.powerplatform.microsoft.us"), + ("dod", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.appsplatform.us"), + ( + "mooncake", + "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.partner.microsoftonline.cn", + ), + ("ex", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.eaglex.ic.gov"), + ("rx", "il-e3064512cc6d4703be71a2ecaecaa98.a.tenant.api.powerplatform.microsoft.scloud"), + ], +) +def test_tenant_island_cluster_endpoint(cluster, expected): + """Test get_tenant_island_cluster_endpoint with real UUID across all clusters.""" + tenant_id = "e3064512-cc6d-4703-be71-a2ecaecaa98a" + disc = PowerPlatformApiDiscovery(cluster) + assert disc.get_tenant_island_cluster_endpoint(tenant_id) == expected + + +# Tests for error handling +@pytest.mark.parametrize( + "tenant_id", + [ + "invalid$name", + "invalid?", + "tenant@id", + "tenant#123", + "tenant with spaces", + "", + "---", + "-", + ], +) +def test_invalid_tenant_identifier(tenant_id): + """Test ValueError is raised for invalid tenant IDs (invalid chars, empty, or all dashes).""" + disc = PowerPlatformApiDiscovery("dev") + with pytest.raises(ValueError): + disc.get_tenant_endpoint(tenant_id) + + +@pytest.mark.parametrize( + "cluster,tenant_id,min_length", + [ + ("local", "a", 2), + ("local", "a-", 2), + ("prod", "aa", 3), + ("prod", "a-a", 3), + ], +) +def test_tenant_identifier_too_short(cluster, tenant_id, min_length): + """Test ValueError is raised when tenant ID is too short after normalization.""" + disc = PowerPlatformApiDiscovery(cluster) + with pytest.raises(ValueError, match=f"must be at least {min_length}"): + disc.get_tenant_endpoint(tenant_id) From 41ce9f54c5f55e4cb78ebbd37c5725972f15d4d8 Mon Sep 17 00:00:00 2001 From: Mrunal Suyoga Hirve <112517572+mrunalhirve128@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:04:23 -0800 Subject: [PATCH 12/14] Update tests/TEST_PLAN.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/TEST_PLAN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TEST_PLAN.md b/tests/TEST_PLAN.md index c8fe3e3d..4d0c6e6c 100644 --- a/tests/TEST_PLAN.md +++ b/tests/TEST_PLAN.md @@ -24,7 +24,7 @@ ## Testing Strategy -**Framework:** `unittest` + `pytest` runner +**Framework:** `pytest` **Coverage:** `pytest-cov` **Mocking:** `unittest.mock` **Async:** `pytest-asyncio` From daf208d90d4bdee0c1e3c3921932973f8c12ab9b Mon Sep 17 00:00:00 2001 From: Mrunal Hirve Date: Tue, 25 Nov 2025 12:05:48 -0800 Subject: [PATCH 13/14] copilot comment --- tests/runtime/test_environment_utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index 77d8c803..7660b173 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -3,7 +3,6 @@ """Unit tests for environment_utils module.""" -import os import pytest from microsoft_agents_a365.runtime.environment_utils import ( @@ -28,15 +27,12 @@ def test_get_observability_authentication_scope(): ("staging", False), ], ) -def test_is_development_environment(env_value, expected): +def test_is_development_environment(monkeypatch, env_value, expected): """Test is_development_environment returns correct value based on PYTHON_ENVIRONMENT.""" if env_value is None: - os.environ.pop("PYTHON_ENVIRONMENT", None) + monkeypatch.delenv("PYTHON_ENVIRONMENT", raising=False) else: - os.environ["PYTHON_ENVIRONMENT"] = env_value + monkeypatch.setenv("PYTHON_ENVIRONMENT", env_value) result = is_development_environment() assert result == expected - - # Cleanup - os.environ.pop("PYTHON_ENVIRONMENT", None) From dc891a39c83b72cef6e2fac6c59d8f4c9c9a9fea Mon Sep 17 00:00:00 2001 From: Mrunal Suyoga Hirve <112517572+mrunalhirve128@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:09:58 -0800 Subject: [PATCH 14/14] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/runtime/test_power_platform_api_discovery.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/runtime/test_power_platform_api_discovery.py b/tests/runtime/test_power_platform_api_discovery.py index 0d7f3641..4189d44c 100644 --- a/tests/runtime/test_power_platform_api_discovery.py +++ b/tests/runtime/test_power_platform_api_discovery.py @@ -3,7 +3,6 @@ """Unit tests for PowerPlatformApiDiscovery class.""" -import re import pytest from microsoft_agents_a365.runtime.power_platform_api_discovery import (