From 0e3f40f1abc7145038180309c70315808bd76690 Mon Sep 17 00:00:00 2001 From: Chris Sanders Date: Mon, 28 Jul 2025 18:20:09 -0500 Subject: [PATCH 1/6] Start task: fix-container-name-conflicts --- tasks/{backlog => active}/fix-container-name-conflicts.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tasks/{backlog => active}/fix-container-name-conflicts.md (100%) diff --git a/tasks/backlog/fix-container-name-conflicts.md b/tasks/active/fix-container-name-conflicts.md similarity index 100% rename from tasks/backlog/fix-container-name-conflicts.md rename to tasks/active/fix-container-name-conflicts.md From ff2c7d451e42dcf6cadfd68d2f4ce8e5200816bc Mon Sep 17 00:00:00 2001 From: Chris Sanders Date: Mon, 28 Jul 2025 18:22:01 -0500 Subject: [PATCH 2/6] Update container naming to use -dev suffix for development builds - Changed default IMAGE_NAME in build script to troubleshoot-mcp-server-dev - Updated test harnesses to expect -dev container images - CI workflow override mechanism remains intact for production builds --- scripts/build.sh | 2 +- tasks/active/fix-container-name-conflicts.md | 6 +++++- tests/conftest.py | 6 +++--- tests/e2e/test_container_bundle_validation.py | 2 +- tests/e2e/test_container_production_validation.py | 6 +++--- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 976068b..4b4a8cd 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -3,7 +3,7 @@ set -euo pipefail # Configuration - use environment variables if set, otherwise use defaults # This allows GitHub Actions to override these values -IMAGE_NAME=${IMAGE_NAME:-"troubleshoot-mcp-server"} +IMAGE_NAME=${IMAGE_NAME:-"troubleshoot-mcp-server-dev"} IMAGE_TAG=${IMAGE_TAG:-"latest"} # Print commands before executing them diff --git a/tasks/active/fix-container-name-conflicts.md b/tasks/active/fix-container-name-conflicts.md index b91d923..ef18ccd 100644 --- a/tasks/active/fix-container-name-conflicts.md +++ b/tasks/active/fix-container-name-conflicts.md @@ -1,9 +1,13 @@ # Fix Container Name Conflicts Between Development and Production -**Status**: backlog +**Status**: active **Priority**: high **Estimated Effort**: 2-4 hours **Category**: infrastructure +**Started**: 2025-07-28 + +## Progress Log +- 2025-07-28: Started task, created worktree, began implementation ## Problem Statement diff --git a/tests/conftest.py b/tests/conftest.py index 147d678..a0dc6e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -174,7 +174,7 @@ def build_container_image(project_root): try: # Remove any existing image first to ensure a clean build subprocess.run( - ["podman", "rmi", "-f", "troubleshoot-mcp-server:latest"], + ["podman", "rmi", "-f", "troubleshoot-mcp-server-dev:latest"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30, @@ -247,11 +247,11 @@ def container_image(request): pytest.skip(f"Failed to build container image: {result.stderr}") # Yield to allow tests to run - yield "troubleshoot-mcp-server:latest" + yield "troubleshoot-mcp-server-dev:latest" # Explicitly clean up any running containers containers_result = subprocess.run( - ["podman", "ps", "-q", "--filter", "ancestor=troubleshoot-mcp-server:latest"], + ["podman", "ps", "-q", "--filter", "ancestor=troubleshoot-mcp-server-dev:latest"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, diff --git a/tests/e2e/test_container_bundle_validation.py b/tests/e2e/test_container_bundle_validation.py index 654f41a..d72cf88 100644 --- a/tests/e2e/test_container_bundle_validation.py +++ b/tests/e2e/test_container_bundle_validation.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) # Container image name (matches build.sh defaults) -CONTAINER_IMAGE = "troubleshoot-mcp-server:latest" +CONTAINER_IMAGE = "troubleshoot-mcp-server-dev:latest" CONTAINER_RUNTIME = "podman" # Could be "docker" if preferred diff --git a/tests/e2e/test_container_production_validation.py b/tests/e2e/test_container_production_validation.py index 18d5275..b9ddd66 100644 --- a/tests/e2e/test_container_production_validation.py +++ b/tests/e2e/test_container_production_validation.py @@ -273,12 +273,12 @@ def test_production_container_mcp_protocol(): # Check if the required container image exists try: result = subprocess.run( - [runtime, "image", "exists", "troubleshoot-mcp-server:latest"], + [runtime, "image", "exists", "troubleshoot-mcp-server-dev:latest"], capture_output=True, ) if result.returncode != 0: pytest.skip( - "Container image troubleshoot-mcp-server:latest not available - build first with: MELANGE_TEST_BUILD=true ./scripts/build.sh" + "Container image troubleshoot-mcp-server-dev:latest not available - build first with: MELANGE_TEST_BUILD=true ./scripts/build.sh" ) except FileNotFoundError: pytest.skip(f"Container runtime {runtime} not found") @@ -309,7 +309,7 @@ def test_production_container_mcp_protocol(): container_name, "--rm", "-i", - "troubleshoot-mcp-server:latest", # Use the built image directly + "troubleshoot-mcp-server-dev:latest", # Use the built image directly ], input=request_json + "\n", capture_output=True, From c2a531f95624a6d99081accc42110bb6b33fb458 Mon Sep 17 00:00:00 2001 From: Chris Sanders Date: Mon, 28 Jul 2025 18:23:27 -0500 Subject: [PATCH 3/6] Update documentation for container variants - Updated PODMAN.md to use correct troubleshoot-mcp-server-dev:latest naming - Added container variants section to README.md explaining dev vs production images - Updated testing documentation with new container name - Corrected all example commands to use development image naming --- PODMAN.md | 12 +++++++----- README.md | 33 ++++++++++++++++++++++++++++++++- docs/TESTING_STRATEGY.md | 2 +- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/PODMAN.md b/PODMAN.md index 8df8946..bcb5c0a 100644 --- a/PODMAN.md +++ b/PODMAN.md @@ -14,7 +14,9 @@ cd troubleshoot-mcp-server ./scripts/build.sh ``` -This will create a Podman image named `mcp-server-troubleshoot:latest`. +This will create a Podman image named `troubleshoot-mcp-server-dev:latest` for local development. + +**Note**: Local development builds use the `-dev` suffix to avoid conflicts with official production releases. The production image is named `troubleshoot-mcp-server:latest`. ## Running the Container @@ -31,7 +33,7 @@ export SBCTL_TOKEN="your_token_here" podman run -i --rm \ -v "$(pwd)/bundles:/data/bundles" \ -e SBCTL_TOKEN="$SBCTL_TOKEN" \ - mcp-server-troubleshoot:latest + troubleshoot-mcp-server-dev:latest ``` ### Command Parameters Explained @@ -92,7 +94,7 @@ To use the Podman container with MCP clients (such as Claude or other AI models) You can get the recommended configuration by running: ```bash -podman run --rm mcp-server-troubleshoot:latest --show-config +podman run --rm troubleshoot-mcp-server-dev:latest --show-config ``` The output will provide a ready-to-use configuration for MCP clients: @@ -110,7 +112,7 @@ The output will provide a ready-to-use configuration for MCP clients: "${HOME}/bundles:/data/bundles", "-e", "SBCTL_TOKEN=${SBCTL_TOKEN}", - "mcp-server-troubleshoot:latest" + "troubleshoot-mcp-server-dev:latest" ] } } @@ -143,7 +145,7 @@ In the Inspector UI: podman run -i --rm \ -v "$(pwd)/bundles:/data/bundles" \ -e SBCTL_TOKEN="$SBCTL_TOKEN" \ - mcp-server-troubleshoot:latest + troubleshoot-mcp-server-dev:latest ``` 4. Click "Save" diff --git a/README.md b/README.md index cbc49ad..d43e2ba 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The easiest way to get started is using Podman: podman run -i --rm \ -v "/path/to/bundles:/data/bundles" \ -e SBCTL_TOKEN="your-token" \ - mcp-server-troubleshoot:latest + troubleshoot-mcp-server-dev:latest ``` See the [Podman documentation](PODMAN.md) for comprehensive container configuration details. @@ -58,6 +58,37 @@ export SBCTL_TOKEN=your-token uv run python -m mcp_server_troubleshoot ``` +## Container Image Variants + +This project provides two distinct container image variants: + +### Development Image: `troubleshoot-mcp-server-dev:latest` +- **Purpose**: Local development and testing +- **Built by**: `./scripts/build.sh` (default) +- **Usage**: When building from source or developing locally +- **Example**: + ```bash + ./scripts/build.sh + podman run -i --rm troubleshoot-mcp-server-dev:latest + ``` + +### Production Image: `troubleshoot-mcp-server:latest` +- **Purpose**: Official releases and production deployments +- **Built by**: CI/CD pipeline with `IMAGE_NAME=troubleshoot-mcp-server ./scripts/build.sh` +- **Usage**: In production environments or when using official releases +- **Example**: + ```bash + IMAGE_NAME=troubleshoot-mcp-server ./scripts/build.sh + podman run -i --rm troubleshoot-mcp-server:latest + ``` + +### Why Two Variants? + +The `-dev` suffix prevents conflicts between local development images and official production releases. This allows users to: +- Use official container releases without interference from local builds +- Develop and test locally without overwriting production images +- Maintain clear separation between development and production environments + ## Documentation For comprehensive documentation, see: diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md index f9b25f1..acf0d8a 100644 --- a/docs/TESTING_STRATEGY.md +++ b/docs/TESTING_STRATEGY.md @@ -257,7 +257,7 @@ uv run pytest tests/e2e/test_direct_tool_integration.py::TestDirectToolIntegrati **Check container logs**: ```bash # Run container interactively -podman run -it --rm troubleshoot-mcp-server:latest /bin/sh +podman run -it --rm troubleshoot-mcp-server-dev:latest /bin/sh ``` ## Future Enhancements From 9d36fa235e1dfaf12f7bc27b24ed2e6a36fcd317 Mon Sep 17 00:00:00 2001 From: Chris Sanders Date: Mon, 28 Jul 2025 18:27:11 -0500 Subject: [PATCH 4/6] Auto-fix code quality issues Run black formatter on all Python files to ensure consistent code style --- debug_fastmcp_lifecycle.py | 4 +- debug_mcp_server.py | 4 +- debug_sbctl.py | 16 +- simple_mcp_test.py | 4 +- src/mcp_server_troubleshoot/__main__.py | 12 +- src/mcp_server_troubleshoot/bundle.py | 321 +++++++++++++----- src/mcp_server_troubleshoot/cli.py | 4 +- src/mcp_server_troubleshoot/files.py | 48 ++- src/mcp_server_troubleshoot/formatters.py | 71 ++-- src/mcp_server_troubleshoot/kubectl.py | 28 +- src/mcp_server_troubleshoot/lifecycle.py | 16 +- src/mcp_server_troubleshoot/server.py | 17 +- .../subprocess_utils.py | 16 +- test_bundle_loading.py | 8 +- test_initialize_bundle_tool.py | 12 +- test_mcp_communication.py | 12 +- test_minimal_mcp.py | 4 +- test_module_startup.py | 8 +- test_production_server.py | 8 +- test_sbctl_direct.py | 8 +- test_simple_mcp.py | 8 +- tests/conftest.py | 20 +- tests/e2e/test_build_reliability.py | 34 +- tests/e2e/test_container_bundle_validation.py | 16 +- .../test_container_production_validation.py | 22 +- .../test_container_shutdown_reliability.py | 8 +- tests/e2e/test_direct_tool_integration.py | 49 ++- tests/e2e/test_mcp_protocol_integration.py | 156 ++++++--- tests/e2e/test_non_container.py | 36 +- tests/e2e/test_podman.py | 16 +- tests/integration/conftest.py | 14 +- tests/integration/mcp_test_utils.py | 19 +- .../integration/test_api_server_lifecycle.py | 42 ++- tests/integration/test_mcp_protocol_errors.py | 4 +- tests/integration/test_real_bundle.py | 98 ++++-- tests/integration/test_server_lifecycle.py | 20 +- .../test_shutdown_race_condition.py | 8 +- .../test_signal_handling_integration.py | 10 +- .../test_subprocess_utilities_integration.py | 20 +- tests/integration/test_tool_functions.py | 30 +- tests/integration/test_url_fetch_auth.py | 32 +- tests/test_all.py | 4 +- tests/test_utils/bundle_helpers.py | 12 +- tests/unit/conftest.py | 18 +- tests/unit/test_bundle.py | 112 ++++-- .../unit/test_bundle_cleanup_dependencies.py | 8 +- tests/unit/test_components.py | 40 ++- .../unit/test_curl_dependency_reproduction.py | 22 +- tests/unit/test_files.py | 80 +++-- tests/unit/test_files_parametrized.py | 4 +- tests/unit/test_fixes_integration.py | 96 ++++-- tests/unit/test_grep_fix.py | 12 +- tests/unit/test_kubectl.py | 56 ++- tests/unit/test_kubectl_parametrized.py | 20 +- tests/unit/test_lifecycle.py | 8 +- tests/unit/test_list_bundles.py | 16 +- tests/unit/test_netstat_dependency.py | 16 +- tests/unit/test_ps_pkill_dependency.py | 24 +- tests/unit/test_python313_transport_issue.py | 16 +- tests/unit/test_server.py | 36 +- tests/unit/test_server_parametrized.py | 48 ++- tests/unit/test_size_limiter.py | 8 +- .../test_transport_cleanup_reproduction.py | 31 +- tests/util/debug_mcp.py | 4 +- 64 files changed, 1409 insertions(+), 565 deletions(-) diff --git a/debug_fastmcp_lifecycle.py b/debug_fastmcp_lifecycle.py index f93f754..54529ae 100644 --- a/debug_fastmcp_lifecycle.py +++ b/debug_fastmcp_lifecycle.py @@ -143,7 +143,9 @@ async def test_server_communication(): # Try to get response with short timeout try: if process.stdout: - response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) + response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=5.0 + ) response_line = response_bytes.decode().strip() print(f"🔍 DEBUG: Got response: {response_line}") except asyncio.TimeoutError: diff --git a/debug_mcp_server.py b/debug_mcp_server.py index 6713efe..e020c65 100644 --- a/debug_mcp_server.py +++ b/debug_mcp_server.py @@ -115,7 +115,9 @@ async def send_request_debug(process, request, timeout=5.0): # Try to get response try: if process.stdout: - response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=timeout) + response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=timeout + ) response_line = response_bytes.decode().strip() print(f"Response: {response_line[:200]}...") diff --git a/debug_sbctl.py b/debug_sbctl.py index 2045fe0..8903d49 100644 --- a/debug_sbctl.py +++ b/debug_sbctl.py @@ -113,7 +113,9 @@ async def debug_sbctl(): # Try to read any output try: if process.stdout: - stdout_data = await asyncio.wait_for(process.stdout.read(), timeout=1.0) + stdout_data = await asyncio.wait_for( + process.stdout.read(), timeout=1.0 + ) if stdout_data: print(f"STDOUT: {stdout_data.decode()}") except asyncio.TimeoutError: @@ -121,7 +123,9 @@ async def debug_sbctl(): try: if process.stderr: - stderr_data = await asyncio.wait_for(process.stderr.read(), timeout=1.0) + stderr_data = await asyncio.wait_for( + process.stderr.read(), timeout=1.0 + ) if stderr_data: print(f"STDERR: {stderr_data.decode()}") except asyncio.TimeoutError: @@ -129,7 +133,9 @@ async def debug_sbctl(): # Check what files were created files_created = list(temp_dir_path.glob("*")) - print(f"\nFiles created in temp directory: {[f.name for f in files_created]}") + print( + f"\nFiles created in temp directory: {[f.name for f in files_created]}" + ) # Check if kubeconfig was created kubeconfig_path = temp_dir_path / "kubeconfig" @@ -138,7 +144,9 @@ async def debug_sbctl(): try: with open(kubeconfig_path, "r") as f: content = f.read() - print(f"Kubeconfig content ({len(content)} chars):\n{content[:500]}...") + print( + f"Kubeconfig content ({len(content)} chars):\n{content[:500]}..." + ) except Exception as e: print(f"Error reading kubeconfig: {e}") else: diff --git a/simple_mcp_test.py b/simple_mcp_test.py index 55b844d..5c39c0a 100644 --- a/simple_mcp_test.py +++ b/simple_mcp_test.py @@ -78,7 +78,9 @@ def test_mcp_server(): import select # Use select to wait for output with timeout - ready, _, _ = select.select([process.stdout], [], [], 5.0) # 5 second timeout + ready, _, _ = select.select( + [process.stdout], [], [], 5.0 + ) # 5 second timeout if ready: response = process.stdout.readline() diff --git a/src/mcp_server_troubleshoot/__main__.py b/src/mcp_server_troubleshoot/__main__.py index 45f61ca..69c0224 100644 --- a/src/mcp_server_troubleshoot/__main__.py +++ b/src/mcp_server_troubleshoot/__main__.py @@ -75,9 +75,15 @@ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: Returns: Parsed arguments """ - parser = argparse.ArgumentParser(description="MCP server for Kubernetes support bundles") - parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") - parser.add_argument("--bundle-dir", type=str, help="Directory to store support bundles") + parser = argparse.ArgumentParser( + description="MCP server for Kubernetes support bundles" + ) + parser.add_argument( + "--verbose", "-v", action="store_true", help="Enable verbose logging" + ) + parser.add_argument( + "--bundle-dir", type=str, help="Directory to store support bundles" + ) parser.add_argument( "--show-config", action="store_true", diff --git a/src/mcp_server_troubleshoot/bundle.py b/src/mcp_server_troubleshoot/bundle.py index e4fed3e..de35809 100644 --- a/src/mcp_server_troubleshoot/bundle.py +++ b/src/mcp_server_troubleshoot/bundle.py @@ -35,11 +35,15 @@ # Feature flags - can be enabled/disabled via environment variables DEFAULT_CLEANUP_ORPHANED = True # Clean up orphaned sbctl processes -DEFAULT_ALLOW_ALTERNATIVE_KUBECONFIG = True # Allow finding kubeconfig in alternative locations +DEFAULT_ALLOW_ALTERNATIVE_KUBECONFIG = ( + True # Allow finding kubeconfig in alternative locations +) # Override with environment variables if provided MAX_DOWNLOAD_SIZE = int(os.environ.get("MAX_DOWNLOAD_SIZE", DEFAULT_DOWNLOAD_SIZE)) -MAX_DOWNLOAD_TIMEOUT = int(os.environ.get("MAX_DOWNLOAD_TIMEOUT", DEFAULT_DOWNLOAD_TIMEOUT)) +MAX_DOWNLOAD_TIMEOUT = int( + os.environ.get("MAX_DOWNLOAD_TIMEOUT", DEFAULT_DOWNLOAD_TIMEOUT) +) MAX_INITIALIZATION_TIMEOUT = int( os.environ.get("MAX_INITIALIZATION_TIMEOUT", DEFAULT_INITIALIZATION_TIMEOUT) ) @@ -80,7 +84,9 @@ def safe_copy_file(src: Union[Path, None], dst: Union[Path, None]) -> None: logger.debug(f"Using MAX_DOWNLOAD_TIMEOUT: {MAX_DOWNLOAD_TIMEOUT} seconds") logger.debug(f"Using MAX_INITIALIZATION_TIMEOUT: {MAX_INITIALIZATION_TIMEOUT} seconds") logger.debug(f"Feature flags - Cleanup orphaned processes: {CLEANUP_ORPHANED}") -logger.debug(f"Feature flags - Allow alternative kubeconfig: {ALLOW_ALTERNATIVE_KUBECONFIG}") +logger.debug( + f"Feature flags - Allow alternative kubeconfig: {ALLOW_ALTERNATIVE_KUBECONFIG}" +) # Constants for Replicated Vendor Portal integration REPLICATED_VENDOR_URL_PATTERN = re.compile( @@ -99,7 +105,9 @@ class BundleMetadata(BaseModel): source: str = Field(description="The source of the bundle (URL or local path)") path: Path = Field(description="The path to the extracted bundle") kubeconfig_path: Path = Field(description="The path to the kubeconfig file") - initialized: bool = Field(description="Whether the bundle has been initialized with sbctl") + initialized: bool = Field( + description="Whether the bundle has been initialized with sbctl" + ) host_only_bundle: bool = Field( False, description="Whether this bundle contains only host resources (no cluster resources)", @@ -202,13 +210,17 @@ class BundleFileInfo(BaseModel): """ path: str = Field(description="The full path to the bundle file") - relative_path: str = Field(description="The relative path without bundle directory prefix") + relative_path: str = Field( + description="The relative path without bundle directory prefix" + ) name: str = Field(description="The name of the bundle file") size_bytes: int = Field(description="The size of the bundle file in bytes") modified_time: float = Field( description="The modification time of the bundle file (seconds since epoch)" ) - valid: bool = Field(description="Whether the bundle appears to be a valid support bundle") + valid: bool = Field( + description="Whether the bundle appears to be a valid support bundle" + ) validation_message: Optional[str] = Field( None, description="Message explaining why the bundle is invalid, if applicable" ) @@ -238,7 +250,9 @@ def __init__(self, bundle_dir: Optional[Path] = None) -> None: self._host_only_bundle: bool = False self._termination_requested: bool = False - async def initialize_bundle(self, source: str, force: bool = False) -> BundleMetadata: + async def initialize_bundle( + self, source: str, force: bool = False + ) -> BundleMetadata: """ Initialize a support bundle from a source. @@ -281,14 +295,19 @@ async def initialize_bundle(self, source: str, force: bool = False) -> BundleMet include_invalid=True ) for bundle in available_bundles: - if bundle.relative_path == source or bundle.name == source: + if ( + bundle.relative_path == source + or bundle.name == source + ): logger.info( f"Found matching bundle by relative path: {bundle.path}" ) bundle_path = Path(bundle.path) break except Exception as e: - logger.warning(f"Error searching for bundle by relative path: {e}") + logger.warning( + f"Error searching for bundle by relative path: {e}" + ) # If we still can't find it, raise an error if not bundle_path.exists(): @@ -304,7 +323,9 @@ async def initialize_bundle(self, source: str, force: bool = False) -> BundleMet bundle_output_dir.mkdir(parents=True, exist_ok=True) # Initialize the bundle with sbctl - kubeconfig_path = await self._initialize_with_sbctl(bundle_path, bundle_output_dir) + kubeconfig_path = await self._initialize_with_sbctl( + bundle_path, bundle_output_dir + ) # Handle case where no kubeconfig was created (host-only bundle) if self._host_only_bundle: @@ -346,7 +367,9 @@ async def initialize_bundle(self, source: str, force: bool = False) -> BundleMet with tarfile.open(bundle_path, "r:gz") as tar: # First list the files to get a count members = tar.getmembers() - logger.info(f"Support bundle contains {len(members)} entries") + logger.info( + f"Support bundle contains {len(members)} entries" + ) # Extract all files from pathlib import PurePath @@ -361,7 +384,9 @@ async def initialize_bundle(self, source: str, force: bool = False) -> BundleMet # Extract with the sanitized member list # Use filter='data' to only extract file data without modifying metadata - tar.extractall(path=extract_dir, members=safe_members, filter="data") + tar.extractall( + path=extract_dir, members=safe_members, filter="data" + ) # List extracted files and verify extraction was successful file_count = 0 @@ -442,7 +467,9 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: # Process the response status and content if response.status_code == 401: - logger.error(f"Replicated API returned 401 Unauthorized for slug {slug}") + logger.error( + f"Replicated API returned 401 Unauthorized for slug {slug}" + ) raise BundleDownloadError( f"Failed to authenticate with Replicated API (status {response.status_code}). " "Check SBCTL_TOKEN/REPLICATED_TOKEN." @@ -469,7 +496,9 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: logger.exception( f"Error decoding JSON response from Replicated API (status 200): {json_e}" ) - raise BundleDownloadError(f"Invalid JSON response from Replicated API: {json_e}") + raise BundleDownloadError( + f"Invalid JSON response from Replicated API: {json_e}" + ) # Add validation: Ensure response_data is a dictionary if not isinstance(response_data, dict): @@ -500,7 +529,9 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: logger.error( f"Missing 'signedUri' in Replicated API response bundle object for slug {slug}. Bundle data: {bundle_data}" ) - raise BundleDownloadError("Could not find 'signedUri' in Replicated API response.") + raise BundleDownloadError( + "Could not find 'signedUri' in Replicated API response." + ) logger.info("Successfully retrieved signed URL from Replicated API.") # Ensure we're returning a string type @@ -513,12 +544,18 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: # Re-raise specific BundleDownloadErrors we've already identified raise e elif isinstance(e, httpx.Timeout): - logger.exception(f"Timeout requesting signed URL from Replicated API: {e}") + logger.exception( + f"Timeout requesting signed URL from Replicated API: {e}" + ) raise BundleDownloadError(f"Timeout requesting signed URL: {e}") from e elif isinstance(e, httpx.RequestError): # This should now correctly catch the RequestError raised by the mock - logger.exception(f"Network error requesting signed URL from Replicated API: {e}") - raise BundleDownloadError(f"Network error requesting signed URL: {e}") from e + logger.exception( + f"Network error requesting signed URL from Replicated API: {e}" + ) + raise BundleDownloadError( + f"Network error requesting signed URL: {e}" + ) from e else: # Catch any other unexpected errors during the entire process and wrap them distinct_error_msg = f"UNEXPECTED EXCEPTION in _get_replicated_signed_url: {type(e).__name__}: {str(e)}" @@ -559,7 +596,9 @@ async def _download_bundle(self, url: str) -> Path: # Catch any other unexpected errors during signed URL retrieval logger.exception(f"Unexpected error getting signed URL for {url}: {e}") # Raise specific error and exit - raise BundleDownloadError(f"Failed to get signed URL for {url}: {str(e)}") + raise BundleDownloadError( + f"Failed to get signed URL for {url}: {str(e)}" + ) # Log the download start *after* potential signed URL retrieval logger.info(f"Starting download from: {actual_download_url[:80]}...") @@ -591,7 +630,9 @@ async def _download_bundle(self, url: str) -> Path: # Headers for the actual download download_headers = {} # Add auth token ONLY for non-Replicated URLs (signed URLs have auth embedded) - if actual_download_url == original_url: # Check if we are using the original URL + if ( + actual_download_url == original_url + ): # Check if we are using the original URL token = os.environ.get("SBCTL_TOKEN") if token: download_headers["Authorization"] = f"Bearer {token}" @@ -607,7 +648,9 @@ async def _download_bundle(self, url: str) -> Path: async with aiohttp.ClientSession(timeout=timeout) as session: # === START MODIFICATION === # Explicitly await the get call first - response_ctx_mgr = session.get(actual_download_url, headers=download_headers) + response_ctx_mgr = session.get( + actual_download_url, headers=download_headers + ) # Now use the awaited response object in the async with async with await response_ctx_mgr as response: # === END MODIFICATION === @@ -654,13 +697,19 @@ async def _download_bundle(self, url: str) -> Path: return download_path except Exception as e: # Use original_url in error messages for clarity - logger.exception(f"Error downloading bundle originally from {original_url}: {str(e)}") + logger.exception( + f"Error downloading bundle originally from {original_url}: {str(e)}" + ) if download_path.exists(): - download_path.unlink(missing_ok=True) # Use missing_ok=True for robustness + download_path.unlink( + missing_ok=True + ) # Use missing_ok=True for robustness # Re-raise BundleDownloadError if it's already that type if isinstance(e, BundleDownloadError): raise - raise BundleDownloadError(f"Failed to download bundle from {original_url}: {str(e)}") + raise BundleDownloadError( + f"Failed to download bundle from {original_url}: {str(e)}" + ) async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> Path: """ @@ -746,7 +795,9 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P logger.info(f"sbctl output: {all_output}") if "No cluster resources found in bundle" in all_output: - logger.info("Bundle contains no cluster resources, marking as host-only bundle") + logger.info( + "Bundle contains no cluster resources, marking as host-only bundle" + ) self._host_only_bundle = True return kubeconfig_path # Return dummy path, file won't exist but that's OK @@ -779,7 +830,9 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P ) if not line: # EOF break - line_text = line.decode("utf-8", errors="replace").strip() + line_text = line.decode( + "utf-8", errors="replace" + ).strip() if line_text: stdout_lines.append(line_text) logger.debug(f"sbctl stdout line: {line_text}") @@ -792,13 +845,17 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P r"export KUBECONFIG=([^\s]+)", line_text ) if kubeconfig_matches: - announced_kubeconfig = Path(kubeconfig_matches[0]) + announced_kubeconfig = Path( + kubeconfig_matches[0] + ) logger.info( f"sbctl announced kubeconfig at: {announced_kubeconfig}" ) # Wait a brief moment for the file to be created - for wait_attempt in range(10): # Wait up to 5 seconds + for wait_attempt in range( + 10 + ): # Wait up to 5 seconds await asyncio.sleep(0.5) if announced_kubeconfig.exists(): logger.info( @@ -824,13 +881,17 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P break if stdout_lines: - logger.debug(f"sbctl initial stdout: {' | '.join(stdout_lines)}") + logger.debug( + f"sbctl initial stdout: {' | '.join(stdout_lines)}" + ) except Exception as read_err: logger.debug(f"Error reading initial stdout: {read_err}") # Continue with normal initialization - logger.debug("sbctl process continuing, proceeding with normal initialization") + logger.debug( + "sbctl process continuing, proceeding with normal initialization" + ) # Wait for initialization to complete await self._wait_for_initialization(kubeconfig_path) @@ -902,7 +963,11 @@ async def _wait_for_initialization( alternative_kubeconfig_paths = [] # Attempt to read process output for diagnostic purposes - if self.sbctl_process and self.sbctl_process.stdout and self.sbctl_process.stderr: + if ( + self.sbctl_process + and self.sbctl_process.stdout + and self.sbctl_process.stderr + ): stdout_data = b"" stderr_data = b"" @@ -944,15 +1009,21 @@ async def _wait_for_initialization( # Extract the kubeconfig path import re - kubeconfig_matches = re.findall(r"export KUBECONFIG=([^\s]+)", stdout_text) + kubeconfig_matches = re.findall( + r"export KUBECONFIG=([^\s]+)", stdout_text + ) if kubeconfig_matches: alt_kubeconfig = Path(kubeconfig_matches[0]) - logger.info(f"Found kubeconfig path in stdout: {alt_kubeconfig}") + logger.info( + f"Found kubeconfig path in stdout: {alt_kubeconfig}" + ) alternative_kubeconfig_paths.append(alt_kubeconfig) # Since we found the kubeconfig path immediately, use it if alt_kubeconfig.exists(): - logger.info(f"Using kubeconfig from stdout: {alt_kubeconfig}") + logger.info( + f"Using kubeconfig from stdout: {alt_kubeconfig}" + ) try: safe_copy_file(alt_kubeconfig, kubeconfig_path) logger.info( @@ -1017,10 +1088,14 @@ async def _wait_for_initialization( try: if self.sbctl_process.stdout: stdout_data = await self.sbctl_process.stdout.read() - process_output += stdout_data.decode("utf-8", errors="replace") + process_output += stdout_data.decode( + "utf-8", errors="replace" + ) if self.sbctl_process.stderr: stderr_data = await self.sbctl_process.stderr.read() - process_output += stderr_data.decode("utf-8", errors="replace") + process_output += stderr_data.decode( + "utf-8", errors="replace" + ) except Exception: pass @@ -1050,7 +1125,9 @@ async def _wait_for_initialization( if not kubeconfig_found and ALLOW_ALTERNATIVE_KUBECONFIG: for alt_path in alternative_kubeconfig_paths: if alt_path.exists(): - logger.info(f"Kubeconfig found at alternative location: {alt_path}") + logger.info( + f"Kubeconfig found at alternative location: {alt_path}" + ) kubeconfig_found = True kubeconfig_found_time = asyncio.get_event_loop().time() found_kubeconfig_path = alt_path @@ -1059,7 +1136,9 @@ async def _wait_for_initialization( try: with open(alt_path, "r") as f: kubeconfig_content = f.read() - logger.debug(f"Alternative kubeconfig content:\n{kubeconfig_content}") + logger.debug( + f"Alternative kubeconfig content:\n{kubeconfig_content}" + ) # Try to copy to expected location try: @@ -1070,7 +1149,9 @@ async def _wait_for_initialization( except Exception as copy_err: logger.warning(f"Failed to copy kubeconfig: {copy_err}") except Exception as e: - logger.warning(f"Failed to read alternative kubeconfig content: {e}") + logger.warning( + f"Failed to read alternative kubeconfig content: {e}" + ) break @@ -1125,7 +1206,9 @@ async def _wait_for_initialization( time_since_kubeconfig = ( asyncio.get_event_loop().time() - kubeconfig_found_time ) - if time_since_kubeconfig > (timeout * api_server_wait_percentage): + if time_since_kubeconfig > ( + timeout * api_server_wait_percentage + ): logger.warning( f"API server not responding after {time_since_kubeconfig:.1f}s " f"({api_server_wait_percentage * 100:.0f}% of timeout). Proceeding anyway." @@ -1157,7 +1240,9 @@ async def _wait_for_initialization( stdout_data = await asyncio.wait_for( self.sbctl_process.stdout.read(), timeout=1.0 ) - process_output += stdout_data.decode("utf-8", errors="replace") + process_output += stdout_data.decode( + "utf-8", errors="replace" + ) except (asyncio.TimeoutError, Exception): pass if self.sbctl_process.stderr: @@ -1165,7 +1250,9 @@ async def _wait_for_initialization( stderr_data = await asyncio.wait_for( self.sbctl_process.stderr.read(), timeout=1.0 ) - process_output += stderr_data.decode("utf-8", errors="replace") + process_output += stderr_data.decode( + "utf-8", errors="replace" + ) except (asyncio.TimeoutError, Exception): pass @@ -1192,7 +1279,9 @@ async def _wait_for_initialization( # Check if this was an intentional termination (SIGTERM/-15) if self.sbctl_process.returncode == -15 and self._termination_requested: - logger.debug("sbctl process was intentionally terminated during cleanup") + logger.debug( + "sbctl process was intentionally terminated during cleanup" + ) return # Exit gracefully without raising an error error_message = f"sbctl process exited with code {self.sbctl_process.returncode} before initialization completed" @@ -1265,7 +1354,9 @@ async def _terminate_sbctl_process(self) -> None: await asyncio.wait_for(self.sbctl_process.wait(), timeout=3.0) logger.debug("sbctl process terminated gracefully") except (asyncio.TimeoutError, ProcessLookupError) as e: - logger.warning(f"Failed to terminate sbctl process gracefully: {str(e)}") + logger.warning( + f"Failed to terminate sbctl process gracefully: {str(e)}" + ) if self.sbctl_process: try: logger.debug("Killing sbctl process...") @@ -1298,14 +1389,18 @@ async def _terminate_sbctl_process(self) -> None: # Check if process is gone os.kill(pid, 0) # If we get here, process still exists, try SIGKILL - logger.debug(f"Process {pid} still exists, sending SIGKILL") + logger.debug( + f"Process {pid} still exists, sending SIGKILL" + ) os.kill(pid, signal.SIGKILL) except ProcessLookupError: logger.debug(f"Process {pid} terminated successfully") except ProcessLookupError: logger.debug(f"Process {pid} not found") except PermissionError: - logger.warning(f"Permission error trying to kill process {pid}") + logger.warning( + f"Permission error trying to kill process {pid}" + ) # Remove the PID file try: @@ -1338,7 +1433,10 @@ async def _terminate_sbctl_process(self) -> None: proc.info["name"] and "sbctl" in proc.info["name"] and proc.info["cmdline"] - and any(bundle_path in arg for arg in proc.info["cmdline"]) + and any( + bundle_path in arg + for arg in proc.info["cmdline"] + ) ): pid = proc.info["pid"] logger.debug( @@ -1346,7 +1444,9 @@ async def _terminate_sbctl_process(self) -> None: ) try: os.kill(pid, signal.SIGTERM) - logger.debug(f"Sent SIGTERM to process {pid}") + logger.debug( + f"Sent SIGTERM to process {pid}" + ) await asyncio.sleep(0.5) # Check if terminated @@ -1365,7 +1465,9 @@ async def _terminate_sbctl_process(self) -> None: ProcessLookupError, PermissionError, ) as e: - logger.debug(f"Error terminating process {pid}: {e}") + logger.debug( + f"Error terminating process {pid}: {e}" + ) except ( psutil.NoSuchProcess, psutil.AccessDenied, @@ -1374,7 +1476,9 @@ async def _terminate_sbctl_process(self) -> None: # Process disappeared or access denied - skip it continue except Exception as e: - logger.warning(f"Error cleaning up orphaned sbctl processes: {e}") + logger.warning( + f"Error cleaning up orphaned sbctl processes: {e}" + ) # As a fallback, try to clean up any sbctl processes related to serve try: @@ -1387,7 +1491,9 @@ async def _terminate_sbctl_process(self) -> None: proc.info["name"] and "sbctl" in proc.info["name"] and proc.info["cmdline"] - and any("serve" in arg for arg in proc.info["cmdline"]) + and any( + "serve" in arg for arg in proc.info["cmdline"] + ) ): try: proc.terminate() @@ -1413,12 +1519,16 @@ async def _terminate_sbctl_process(self) -> None: else: logger.debug("No sbctl serve processes found to terminate") except Exception as e: - logger.warning(f"Error using psutil to terminate sbctl processes: {e}") + logger.warning( + f"Error using psutil to terminate sbctl processes: {e}" + ) except Exception as e: logger.warning(f"Error during extended cleanup: {e}") else: - logger.debug("Skipping orphaned process cleanup (disabled by configuration)") + logger.debug( + "Skipping orphaned process cleanup (disabled by configuration)" + ) async def _cleanup_active_bundle(self) -> None: """ @@ -1456,7 +1566,9 @@ async def _cleanup_active_bundle(self) -> None: files = glob.glob(f"{bundle_path}/**", recursive=True) logger.info(f"Found {len(files)} items in bundle directory") except Exception as list_err: - logger.warning(f"Error getting bundle directory details: {list_err}") + logger.warning( + f"Error getting bundle directory details: {list_err}" + ) # Create a list of paths we should not delete (containing parent directories) protected_paths = [ @@ -1472,7 +1584,9 @@ async def _cleanup_active_bundle(self) -> None: try: import shutil - logger.info(f"Starting shutil.rmtree on bundle path: {bundle_path}") + logger.info( + f"Starting shutil.rmtree on bundle path: {bundle_path}" + ) shutil.rmtree(bundle_path) logger.info( "shutil.rmtree completed, checking if path still exists" @@ -1487,7 +1601,9 @@ async def _cleanup_active_bundle(self) -> None: f"Successfully removed bundle directory: {bundle_path}" ) except PermissionError as e: - logger.error(f"Permission error removing bundle directory: {e}") + logger.error( + f"Permission error removing bundle directory: {e}" + ) logger.error( f"Error details: {str(e)}, file: {getattr(e, 'filename', 'unknown')}" ) @@ -1506,7 +1622,9 @@ async def _cleanup_active_bundle(self) -> None: f"Bundle path {bundle_path} is a protected path, not removing" ) if not bundle_path.exists(): - logger.warning(f"Bundle path {bundle_path} no longer exists") + logger.warning( + f"Bundle path {bundle_path} no longer exists" + ) else: if not self.active_bundle.path: logger.warning("Active bundle path is None") @@ -1553,7 +1671,9 @@ def _generate_bundle_id(self, source: str) -> str: sanitized = "bundle" # Add randomness to ensure uniqueness - random_suffix = os.urandom(8).hex() # Increased from 4 to 8 bytes for more entropy + random_suffix = os.urandom( + 8 + ).hex() # Increased from 4 to 8 bytes for more entropy return f"{sanitized}_{random_suffix}" @@ -1600,7 +1720,9 @@ async def check_api_server_available(self) -> bool: # Try to find kubeconfig in current directory (where sbctl might create it) current_dir_kubeconfig = Path.cwd() / "kubeconfig" if current_dir_kubeconfig.exists(): - logger.info(f"Found kubeconfig in current directory: {current_dir_kubeconfig}") + logger.info( + f"Found kubeconfig in current directory: {current_dir_kubeconfig}" + ) kubeconfig_path = current_dir_kubeconfig # Try to parse kubeconfig if found @@ -1610,7 +1732,9 @@ async def check_api_server_available(self) -> bool: with open(kubeconfig_path, "r") as f: kubeconfig_content = f.read() - logger.debug(f"Kubeconfig content (first 200 chars): {kubeconfig_content[:200]}...") + logger.debug( + f"Kubeconfig content (first 200 chars): {kubeconfig_content[:200]}..." + ) # Try parsing as JSON first, then try YAML or manual parsing as fallback config = {} @@ -1638,8 +1762,12 @@ async def check_api_server_available(self) -> bool: ) if server_matches: server_url = server_matches[0].strip() - config = {"clusters": [{"cluster": {"server": server_url}}]} - logger.debug(f"Extracted server URL using regex: {server_url}") + config = { + "clusters": [{"cluster": {"server": server_url}}] + } + logger.debug( + f"Extracted server URL using regex: {server_url}" + ) else: logger.warning( "Could not extract server URL from kubeconfig with regex" @@ -1679,7 +1807,9 @@ async def check_api_server_available(self) -> bool: port = int(server_url.split(":")[-1]) logger.debug(f"Extracted API server port directly: {port}") except (ValueError, IndexError) as e: - logger.warning(f"Failed to extract port from server URL: {e}") + logger.warning( + f"Failed to extract port from server URL: {e}" + ) except (json.JSONDecodeError, KeyError, ValueError, IndexError) as e: logger.warning(f"Failed to parse kubeconfig: {e}") @@ -1699,9 +1829,13 @@ async def check_api_server_available(self) -> bool: from .subprocess_utils import pipe_transport_reader try: - async with pipe_transport_reader(self.sbctl_process.stdout) as stdout_reader: + async with pipe_transport_reader( + self.sbctl_process.stdout + ) as stdout_reader: # Set a timeout for reading - data = await asyncio.wait_for(stdout_reader.read(1024), timeout=0.5) + data = await asyncio.wait_for( + stdout_reader.read(1024), timeout=0.5 + ) if data: output = data.decode("utf-8", errors="replace") logger.debug(f"sbctl process output: {output}") @@ -1721,7 +1855,9 @@ async def check_api_server_available(self) -> bool: parsed_url = urlparse(url) if parsed_url.port: port = parsed_url.port - logger.debug(f"Using port from sbctl output: {port}") + logger.debug( + f"Using port from sbctl output: {port}" + ) if parsed_url.hostname: host = parsed_url.hostname except Exception: @@ -1757,7 +1893,9 @@ async def check_api_server_available(self) -> bool: # Get response body for debugging try: - body = await asyncio.wait_for(response.text(), timeout=1.0) + body = await asyncio.wait_for( + response.text(), timeout=1.0 + ) logger.debug( f"Response from {url} (first 200 chars): {body[:200]}..." ) @@ -1781,7 +1919,9 @@ async def check_api_server_available(self) -> bool: async with aiohttp.ClientSession(timeout=backup_timeout) as session: for endpoint in endpoints: url = f"http://{host}:{port}{endpoint}" - logger.debug(f"Checking API server with backup aiohttp check: {url}") + logger.debug( + f"Checking API server with backup aiohttp check: {url}" + ) try: async with session.get(url) as response: @@ -1837,7 +1977,8 @@ async def get_diagnostic_info(self) -> dict[str, object]: "sbctl_process_running": self.sbctl_process is not None and self.sbctl_process.returncode is None, "api_server_available": await self.check_api_server_available(), - "bundle_initialized": self.active_bundle is not None and self.active_bundle.initialized, + "bundle_initialized": self.active_bundle is not None + and self.active_bundle.initialized, "system_info": await self._get_system_info(), } @@ -1942,7 +2083,9 @@ async def _get_system_info(self) -> dict[str, object]: except Exception as e: info["socket_port_check_exception"] = str(e) - logger.warning(f"Error during Python socket port check for port {port}: {e}") + logger.warning( + f"Error during Python socket port check for port {port}: {e}" + ) # Fallback: assume port is not listening if we can't check info[f"port_{port}_listening"] = False info[f"port_{port}_details"] = f"Could not check port {port}: {e}" @@ -1959,7 +2102,9 @@ async def _get_system_info(self) -> dict[str, object]: try: body = await asyncio.wait_for(response.text(), timeout=1.0) if body: - info[f"http_{port}_response_body"] = body[:200] # Limit body size + info[f"http_{port}_response_body"] = body[ + :200 + ] # Limit body size except (asyncio.TimeoutError, UnicodeDecodeError): pass except (aiohttp.ClientError, asyncio.TimeoutError) as e: @@ -1972,7 +2117,9 @@ async def _get_system_info(self) -> dict[str, object]: return info - async def list_available_bundles(self, include_invalid: bool = False) -> List[BundleFileInfo]: + async def list_available_bundles( + self, include_invalid: bool = False + ) -> List[BundleFileInfo]: """ List available support bundles in the bundle storage directory. @@ -2015,12 +2162,16 @@ async def list_available_bundles(self, include_invalid: bool = False) -> List[Bu try: valid, validation_message = self._check_bundle_validity(file_path) except Exception as e: - logger.warning(f"Error checking bundle validity for {file_path}: {str(e)}") + logger.warning( + f"Error checking bundle validity for {file_path}: {str(e)}" + ) validation_message = f"Error checking validity: {str(e)}" # Skip invalid bundles if requested if not valid and not include_invalid: - logger.debug(f"Skipping invalid bundle {file_path}: {validation_message}") + logger.debug( + f"Skipping invalid bundle {file_path}: {validation_message}" + ) continue # Create the bundle info @@ -2048,9 +2199,15 @@ async def list_available_bundles(self, include_invalid: bool = False) -> List[Bu path=str(file_path), relative_path=file_path.name, name=file_path.name, - size_bytes=(file_path.stat().st_size if file_path.exists() else 0), + size_bytes=( + file_path.stat().st_size + if file_path.exists() + else 0 + ), modified_time=( - file_path.stat().st_mtime if file_path.exists() else 0 + file_path.stat().st_mtime + if file_path.exists() + else 0 ), valid=False, validation_message=f"Error: {str(e)}", @@ -2099,7 +2256,9 @@ def _check_bundle_validity(self, file_path: Path) -> Tuple[bool, Optional[str]]: try: with tarfile.open(file_path, "r:gz") as tar: # List first few entries to check structure without extracting - members = tar.getmembers()[:20] # Just check the first 20 entries for efficiency + members = tar.getmembers()[ + :20 + ] # Just check the first 20 entries for efficiency # Look for patterns that indicate a support bundle has_cluster_resources = False @@ -2178,7 +2337,9 @@ async def cleanup(self) -> None: try: proc.terminate() terminated_count += 1 - logger.debug(f"Terminated sbctl process with PID {proc.pid}") + logger.debug( + f"Terminated sbctl process with PID {proc.pid}" + ) except (psutil.NoSuchProcess, psutil.AccessDenied): # Process already gone or access denied - skip it continue @@ -2199,7 +2360,9 @@ async def cleanup(self) -> None: try: logger.info(f"Removing temporary bundle directory: {self.bundle_dir}") shutil.rmtree(self.bundle_dir) - logger.info(f"Successfully removed temporary bundle directory: {self.bundle_dir}") + logger.info( + f"Successfully removed temporary bundle directory: {self.bundle_dir}" + ) except Exception as e: logger.error(f"Failed to remove temporary bundle directory: {str(e)}") diff --git a/src/mcp_server_troubleshoot/cli.py b/src/mcp_server_troubleshoot/cli.py index 30d4c07..3dbb54c 100644 --- a/src/mcp_server_troubleshoot/cli.py +++ b/src/mcp_server_troubleshoot/cli.py @@ -58,7 +58,9 @@ def setup_logging(verbose: bool = False, mcp_mode: bool = False) -> None: def parse_args() -> argparse.Namespace: """Parse command-line arguments for the MCP server.""" - parser = argparse.ArgumentParser(description="MCP server for Kubernetes support bundles") + parser = argparse.ArgumentParser( + description="MCP server for Kubernetes support bundles" + ) parser.add_argument("--bundle-dir", type=Path, help="Directory to store bundles") parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") parser.add_argument( diff --git a/src/mcp_server_troubleshoot/files.py b/src/mcp_server_troubleshoot/files.py index 1288273..54dccf7 100644 --- a/src/mcp_server_troubleshoot/files.py +++ b/src/mcp_server_troubleshoot/files.py @@ -101,7 +101,9 @@ class ReadFileArgs(BaseModel): """ path: str = Field(description="The path to the file within the bundle") - start_line: int = Field(0, description="The line number to start reading from (0-indexed)") + start_line: int = Field( + 0, description="The line number to start reading from (0-indexed)" + ) end_line: Optional[int] = Field( None, description="The line number to end reading at (0-indexed, inclusive)" ) @@ -155,10 +157,16 @@ class GrepFilesArgs(BaseModel): pattern: str = Field(description="The pattern to search for") path: str = Field(description="The path within the bundle to search") recursive: bool = Field(True, description="Whether to search recursively") - glob_pattern: Optional[str] = Field(None, description="The glob pattern to match files against") - case_sensitive: bool = Field(False, description="Whether the search is case-sensitive") + glob_pattern: Optional[str] = Field( + None, description="The glob pattern to match files against" + ) + case_sensitive: bool = Field( + False, description="Whether the search is case-sensitive" + ) max_results: int = Field(1000, description="Maximum number of results to return") - max_results_per_file: int = Field(5, description="Maximum number of results to return per file") + max_results_per_file: int = Field( + 5, description="Maximum number of results to return per file" + ) max_files: int = Field(10, description="Maximum number of files to search/return") verbosity: Optional[str] = Field( None, @@ -222,14 +230,20 @@ class FileInfo(BaseModel): """ name: str = Field(description="The name of the file or directory") - path: str = Field(description="The path of the file or directory relative to the bundle root") + path: str = Field( + description="The path of the file or directory relative to the bundle root" + ) type: str = Field(description="The type of the entry ('file' or 'dir')") size: int = Field(description="The size of the file in bytes (0 for directories)") - access_time: float = Field(description="The time of most recent access (seconds since epoch)") + access_time: float = Field( + description="The time of most recent access (seconds since epoch)" + ) modify_time: float = Field( description="The time of most recent content modification (seconds since epoch)" ) - is_binary: bool = Field(description="Whether the file appears to be binary (for files)") + is_binary: bool = Field( + description="Whether the file appears to be binary (for files)" + ) class FileListResult(BaseModel): @@ -251,7 +265,9 @@ class FileContentResult(BaseModel): path: str = Field(description="The path of the file that was read") content: str = Field(description="The content of the file") - start_line: int = Field(description="The line number that was started from (0-indexed)") + start_line: int = Field( + description="The line number that was started from (0-indexed)" + ) end_line: int = Field(description="The line number that was ended at (0-indexed)") total_lines: int = Field(description="The total number of lines in the file") binary: bool = Field(description="Whether the file appears to be binary") @@ -276,12 +292,16 @@ class GrepResult(BaseModel): pattern: str = Field(description="The pattern that was searched for") path: str = Field(description="The path that was searched") - glob_pattern: Optional[str] = Field(description="The glob pattern that was used, if any") + glob_pattern: Optional[str] = Field( + description="The glob pattern that was used, if any" + ) matches: List[GrepMatch] = Field(description="The matches found") total_matches: int = Field(description="The total number of matches found") files_searched: int = Field(description="The number of files that were searched") case_sensitive: bool = Field(description="Whether the search was case-sensitive") - truncated: bool = Field(description="Whether the results were truncated due to max_results") + truncated: bool = Field( + description="Whether the results were truncated due to max_results" + ) files_truncated: bool = Field( default=False, description="Whether the file list was truncated due to max_files", @@ -332,7 +352,9 @@ def _get_bundle_path(self) -> Path: if support_bundle_dirs: support_bundle_dir = support_bundle_dirs[0] # Use the first one found if support_bundle_dir.exists() and support_bundle_dir.is_dir(): - logger.debug(f"Using extracted bundle subdirectory: {support_bundle_dir}") + logger.debug( + f"Using extracted bundle subdirectory: {support_bundle_dir}" + ) return support_bundle_dir # If no support-bundle-* directory, check if the extracted directory itself has files @@ -551,7 +573,9 @@ async def read_file( # For binary files, just read the whole file if is_binary: if start_line > 0 or end_line is not None: - logger.warning("Line range filtering not supported for binary files") + logger.warning( + "Line range filtering not supported for binary files" + ) content = f.read() if isinstance(content, bytes): # For binary, return a hex dump diff --git a/src/mcp_server_troubleshoot/formatters.py b/src/mcp_server_troubleshoot/formatters.py index 2bfc779..f55ce3d 100644 --- a/src/mcp_server_troubleshoot/formatters.py +++ b/src/mcp_server_troubleshoot/formatters.py @@ -67,7 +67,9 @@ def format_bundle_initialization( if api_server_available: return json.dumps({"bundle_id": metadata.id, "status": "ready"}) else: - return json.dumps({"bundle_id": metadata.id, "status": "api_unavailable"}) + return json.dumps( + {"bundle_id": metadata.id, "status": "api_unavailable"} + ) elif self.verbosity == VerbosityLevel.STANDARD: result = { @@ -132,9 +134,9 @@ def format_bundle_list(self, bundles: List[BundleFileInfo]) -> str: # Format modification time import datetime - modified_time_str = datetime.datetime.fromtimestamp(bundle.modified_time).strftime( - "%Y-%m-%d %H:%M:%S" - ) + modified_time_str = datetime.datetime.fromtimestamp( + bundle.modified_time + ).strftime("%Y-%m-%d %H:%M:%S") bundle_entry = { "name": bundle.name, @@ -153,15 +155,21 @@ def format_bundle_list(self, bundles: List[BundleFileInfo]) -> str: bundle_list.append(bundle_entry) response_obj = {"bundles": bundle_list, "total": len(bundle_list)} - response = f"```json\n{json.dumps(response_obj, separators=(',', ':'))}\n```\n\n" + response = ( + f"```json\n{json.dumps(response_obj, separators=(',', ':'))}\n```\n\n" + ) # Add usage instructions - example_bundle = next((b for b in bundles if b.valid), bundles[0] if bundles else None) + example_bundle = next( + (b for b in bundles if b.valid), bundles[0] if bundles else None + ) if example_bundle: response += "## Usage Instructions\n\n" response += "To use one of these bundles, initialize it with the `initialize_bundle` tool using the `source` value:\n\n" response += ( - '```json\n{\n "source": "' + example_bundle.relative_path + '"\n}\n```\n\n' + '```json\n{\n "source": "' + + example_bundle.relative_path + + '"\n}\n```\n\n' ) response += "After initializing a bundle, you can explore its contents using the file exploration tools (`list_files`, `read_file`, `grep_files`) and run kubectl commands with the `kubectl` tool." @@ -172,7 +180,10 @@ def format_file_list(self, result: FileListResult) -> str: if self.verbosity == VerbosityLevel.MINIMAL: return json.dumps( - [entry.name + ("/" if entry.type == "dir" else "") for entry in result.entries] + [ + entry.name + ("/" if entry.type == "dir" else "") + for entry in result.entries + ] ) elif self.verbosity == VerbosityLevel.STANDARD: @@ -236,7 +247,9 @@ def format_file_content(self, result: FileContentResult) -> str: response = f"Read {file_type} file {result.path} (lines {result.start_line + 1}-{result.end_line + 1} of {result.total_lines}):\n" response += f"```\n{content_with_numbers}```" else: - response = f"Read {file_type} file {result.path} (binary data shown as hex):\n" + response = ( + f"Read {file_type} file {result.path} (binary data shown as hex):\n" + ) response += f"```\n{result.content}\n```" return response @@ -299,7 +312,9 @@ def format_grep_results(self, result: GrepResult) -> str: else: # VERBOSE or DEBUG # Current full format - pattern_type = "case-sensitive" if result.case_sensitive else "case-insensitive" + pattern_type = ( + "case-sensitive" if result.case_sensitive else "case-insensitive" + ) path_desc = result.path + ( f" (matching {result.glob_pattern})" if result.glob_pattern else "" ) @@ -351,9 +366,13 @@ def format_kubectl_result(self, result: KubectlResult) -> str: elif self.verbosity == VerbosityLevel.STANDARD: if result.is_json: - return json.dumps({"output": result.output, "exit_code": result.exit_code}) + return json.dumps( + {"output": result.output, "exit_code": result.exit_code} + ) else: - return json.dumps({"output": result.stdout, "exit_code": result.exit_code}) + return json.dumps( + {"output": result.stdout, "exit_code": result.exit_code} + ) else: # VERBOSE or DEBUG # Current full format @@ -362,7 +381,9 @@ def format_kubectl_result(self, result: KubectlResult) -> str: response = f"kubectl command executed successfully:\n```json\n{output_str}\n```" else: output_str = result.stdout - response = f"kubectl command executed successfully:\n```\n{output_str}\n```" + response = ( + f"kubectl command executed successfully:\n```\n{output_str}\n```" + ) metadata = { "command": result.command, @@ -378,7 +399,9 @@ def format_kubectl_result(self, result: KubectlResult) -> str: return response - def format_error(self, error_message: str, diagnostics: Optional[Dict[str, Any]] = None) -> str: + def format_error( + self, error_message: str, diagnostics: Optional[Dict[str, Any]] = None + ) -> str: """Format error messages based on verbosity level.""" if self.verbosity == VerbosityLevel.MINIMAL: @@ -394,7 +417,9 @@ def format_error(self, error_message: str, diagnostics: Optional[Dict[str, Any]] response += f"\n\nDiagnostic information:\n```json\n{json.dumps(diagnostics, separators=(',', ':'))}\n```" return response - def format_overflow_message(self, tool_name: str, estimated_tokens: int, content: str) -> str: + def format_overflow_message( + self, tool_name: str, estimated_tokens: int, content: str + ) -> str: """ Format helpful overflow message with tool-specific guidance. @@ -452,8 +477,12 @@ def _get_tool_specific_suggestions(self, tool_name: str) -> List[str]: "Target specific namespaces: kubectl get pods -n specific-namespace", "Limit results: kubectl get pods --limit=50", ], - "initialize_bundle": ['Use minimal verbosity: initialize_bundle(verbosity="minimal")'], - "list_bundles": ['Use minimal verbosity: list_bundles(verbosity="minimal")'], + "initialize_bundle": [ + 'Use minimal verbosity: initialize_bundle(verbosity="minimal")' + ], + "list_bundles": [ + 'Use minimal verbosity: list_bundles(verbosity="minimal")' + ], } return suggestions_map.get( @@ -494,16 +523,16 @@ def _generate_content_preview(self, content: str) -> str: lines = content.split("\n") total_lines = len(lines) - preview_msg = ( - f"Showing first {preview_chars} characters (content has {total_lines:,} lines):\n" - ) + preview_msg = f"Showing first {preview_chars} characters (content has {total_lines:,} lines):\n" preview_msg += "```\n" + preview_text + "\n```\n" # Add helpful context about the content structure if content.strip().startswith("{") or content.strip().startswith("["): preview_msg += "\n*Note: Content appears to be JSON format*\n" elif "```" in content: - preview_msg += "\n*Note: Content contains code blocks or formatted sections*\n" + preview_msg += ( + "\n*Note: Content contains code blocks or formatted sections*\n" + ) return preview_msg diff --git a/src/mcp_server_troubleshoot/kubectl.py b/src/mcp_server_troubleshoot/kubectl.py index 39d252b..142ae9e 100644 --- a/src/mcp_server_troubleshoot/kubectl.py +++ b/src/mcp_server_troubleshoot/kubectl.py @@ -82,7 +82,9 @@ def validate_command(cls, v: str) -> str: ] for op in dangerous_operations: if re.search(rf"^\s*{op}\b", v): - raise ValueError(f"Kubectl command '{op}' is not allowed for safety reasons") + raise ValueError( + f"Kubectl command '{op}' is not allowed for safety reasons" + ) return v @@ -98,7 +100,9 @@ class KubectlResult(BaseModel): stderr: str = Field(description="The standard error output of the command") output: Any = Field(description="The parsed output, if applicable") is_json: bool = Field(description="Whether the output is JSON") - duration_ms: int = Field(description="The duration of the command execution in milliseconds") + duration_ms: int = Field( + description="The duration of the command execution in milliseconds" + ) @field_validator("exit_code") @classmethod @@ -162,7 +166,9 @@ async def execute( ) # Construct the command - return await self._run_kubectl_command(command, active_bundle, timeout, json_output) + return await self._run_kubectl_command( + command, active_bundle, timeout, json_output + ) async def _run_kubectl_command( self, command: str, bundle: BundleMetadata, timeout: int, json_output: bool @@ -226,7 +232,9 @@ async def _run_kubectl_command( stderr_str = stderr.decode("utf-8") # Process the output - output, is_json = self._process_output(stdout_str, returncode == 0 and json_output) + output, is_json = self._process_output( + stdout_str, returncode == 0 and json_output + ) # Create the result result = KubectlResult( @@ -241,16 +249,22 @@ async def _run_kubectl_command( # Log the result if returncode == 0: - logger.info(f"kubectl command completed successfully in {duration_ms}ms") + logger.info( + f"kubectl command completed successfully in {duration_ms}ms" + ) else: - logger.error(f"kubectl command failed with exit code {returncode}: {stderr_str}") + logger.error( + f"kubectl command failed with exit code {returncode}: {stderr_str}" + ) raise KubectlError("kubectl command failed", returncode, stderr_str) return result except (OSError, FileNotFoundError) as e: logger.exception(f"Error executing kubectl command: {str(e)}") - raise KubectlError("Failed to execute kubectl command", 1, f"Error: {str(e)}") + raise KubectlError( + "Failed to execute kubectl command", 1, f"Error: {str(e)}" + ) def _process_output(self, output: str, try_json: bool) -> Tuple[Any, bool]: """ diff --git a/src/mcp_server_troubleshoot/lifecycle.py b/src/mcp_server_troubleshoot/lifecycle.py index 181ac06..11efb68 100644 --- a/src/mcp_server_troubleshoot/lifecycle.py +++ b/src/mcp_server_troubleshoot/lifecycle.py @@ -56,7 +56,9 @@ def create_temp_directory() -> str: return temp_dir -async def periodic_bundle_cleanup(bundle_manager: BundleManager, interval: int = 3600) -> None: +async def periodic_bundle_cleanup( + bundle_manager: BundleManager, interval: int = 3600 +) -> None: """Periodically clean up old bundles.""" logger.info(f"Starting periodic bundle cleanup (interval: {interval}s)") try: @@ -92,7 +94,9 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: bundle_dir_str = os.environ.get("MCP_BUNDLE_STORAGE") bundle_dir = Path(bundle_dir_str) if bundle_dir_str else None - enable_periodic_cleanup = os.environ.get("ENABLE_PERIODIC_CLEANUP", "false").lower() in ( + enable_periodic_cleanup = os.environ.get( + "ENABLE_PERIODIC_CLEANUP", "false" + ).lower() in ( "true", "1", "yes", @@ -116,7 +120,9 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # Start periodic cleanup task if configured if enable_periodic_cleanup: - logger.info(f"Enabling periodic bundle cleanup every {cleanup_interval} seconds") + logger.info( + f"Enabling periodic bundle cleanup every {cleanup_interval} seconds" + ) background_tasks["bundle_cleanup"] = asyncio.create_task( periodic_bundle_cleanup(bundle_manager, cleanup_interval) ) @@ -157,7 +163,9 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: try: await asyncio.wait_for(asyncio.shield(task), timeout=5.0) except (asyncio.CancelledError, asyncio.TimeoutError): - logger.warning(f"Task {name} did not complete gracefully within timeout") + logger.warning( + f"Task {name} did not complete gracefully within timeout" + ) # Clean up bundle manager resources try: diff --git a/src/mcp_server_troubleshoot/server.py b/src/mcp_server_troubleshoot/server.py index 7af7398..e62356b 100644 --- a/src/mcp_server_troubleshoot/server.py +++ b/src/mcp_server_troubleshoot/server.py @@ -183,7 +183,9 @@ async def initialize_bundle(args: InitializeBundleArgs) -> List[TextContent]: diagnostics = await bundle_manager.get_diagnostic_info() # Format response using the formatter - response = formatter.format_bundle_initialization(result, api_server_available, diagnostics) + response = formatter.format_bundle_initialization( + result, api_server_available, diagnostics + ) return check_response_size(response, "initialize_bundle", formatter) except BundleManagerError as e: @@ -314,7 +316,9 @@ async def kubectl(args: KubectlCommandArgs) -> List[TextContent]: return check_response_size(formatted_error, "kubectl", formatter) # Execute the kubectl command - result = await get_kubectl_executor().execute(args.command, args.timeout, args.json_output) + result = await get_kubectl_executor().execute( + args.command, args.timeout, args.json_output + ) # Format response using the formatter response = formatter.format_kubectl_result(result) @@ -330,7 +334,10 @@ async def kubectl(args: KubectlCommandArgs) -> List[TextContent]: diagnostics = await bundle_manager.get_diagnostic_info() # Check if this is a connection issue - if "connection refused" in str(e).lower() or "could not connect" in str(e).lower(): + if ( + "connection refused" in str(e).lower() + or "could not connect" in str(e).lower() + ): error_message += ( " This appears to be a connection issue with the Kubernetes API server. " "The API server may not be running properly. " @@ -443,7 +450,9 @@ async def read_file(args: ReadFileArgs) -> List[TextContent]: formatter = get_formatter(args.verbosity) try: - result = await get_file_explorer().read_file(args.path, args.start_line, args.end_line) + result = await get_file_explorer().read_file( + args.path, args.start_line, args.end_line + ) response = formatter.format_file_content(result) return check_response_size(response, "read_file", formatter) diff --git a/src/mcp_server_troubleshoot/subprocess_utils.py b/src/mcp_server_troubleshoot/subprocess_utils.py index 48bd972..9e7f189 100644 --- a/src/mcp_server_troubleshoot/subprocess_utils.py +++ b/src/mcp_server_troubleshoot/subprocess_utils.py @@ -50,7 +50,9 @@ def _safe_transport_cleanup(transport: Any) -> None: is_closing = transport.is_closing() logger.debug(f"Transport is_closing status: {is_closing}") else: - logger.debug("Transport doesn't have is_closing method, assuming closed") + logger.debug( + "Transport doesn't have is_closing method, assuming closed" + ) except AttributeError as e: # This is the specific error we're trying to avoid logger.debug( @@ -149,7 +151,9 @@ async def pipe_transport_reader( _safe_transport_cleanup(transport) # Wait for transport to close safely - await _safe_transport_wait_close(transport, timeout_per_check=0.1, max_checks=10) + await _safe_transport_wait_close( + transport, timeout_per_check=0.1, max_checks=10 + ) async def subprocess_exec_with_cleanup( @@ -191,7 +195,9 @@ async def subprocess_exec_with_cleanup( process = await asyncio.create_subprocess_exec(*args, **subprocess_kwargs) if timeout is not None: - stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) + stdout, stderr = await asyncio.wait_for( + process.communicate(), timeout=timeout + ) else: stdout, stderr = await process.communicate() @@ -270,7 +276,9 @@ async def subprocess_shell_with_cleanup( process = await asyncio.create_subprocess_shell(command, **subprocess_kwargs) if timeout is not None: - stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) + stdout, stderr = await asyncio.wait_for( + process.communicate(), timeout=timeout + ) else: stdout, stderr = await process.communicate() diff --git a/test_bundle_loading.py b/test_bundle_loading.py index 0d57f9d..716f49e 100644 --- a/test_bundle_loading.py +++ b/test_bundle_loading.py @@ -49,7 +49,9 @@ async def test_bundle_loading(): await client.initialize_mcp() print("✅ MCP initialized") - print("\n=== Step 3: Testing initialize_bundle tool (with timeout tracking) ===") + print( + "\n=== Step 3: Testing initialize_bundle tool (with timeout tracking) ===" + ) print(f"Calling initialize_bundle with path: {test_bundle_copy}") # Add timeout tracking to see where it gets stuck @@ -60,7 +62,9 @@ async def test_bundle_loading(): try: print("Sending tool call request...") content = await asyncio.wait_for( - client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}), + client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ), timeout=30.0, # 30 second timeout to see what happens ) elapsed = time.time() - start_time diff --git a/test_initialize_bundle_tool.py b/test_initialize_bundle_tool.py index f11dfb8..8b8bbb1 100644 --- a/test_initialize_bundle_tool.py +++ b/test_initialize_bundle_tool.py @@ -75,7 +75,9 @@ async def test_initialize_bundle_tool(): # Read initialize response if process.stdout: - init_response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) + init_response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=5.0 + ) init_response = init_response_bytes.decode().strip() print(f"Initialize response: {init_response}") @@ -101,7 +103,9 @@ async def test_initialize_bundle_tool(): # Try to read response with timeout try: if process.stdout: - print("Waiting for tool response (this should complete in ~6 seconds)...") + print( + "Waiting for tool response (this should complete in ~6 seconds)..." + ) response_bytes = await asyncio.wait_for( process.stdout.readline(), timeout=30.0, # Generous timeout @@ -126,7 +130,9 @@ async def test_initialize_bundle_tool(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for(process.stderr.read(4096), timeout=1.0) + stderr_data = await asyncio.wait_for( + process.stderr.read(4096), timeout=1.0 + ) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR during timeout: {stderr_text}") diff --git a/test_mcp_communication.py b/test_mcp_communication.py index 7ef84bd..6d1067f 100644 --- a/test_mcp_communication.py +++ b/test_mcp_communication.py @@ -15,7 +15,9 @@ sys.path.insert(0, str(Path(__file__).parent)) # Set up minimal logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) async def test_mcp_communication(): @@ -65,7 +67,9 @@ async def test_mcp_communication(): # Try to read response with timeout try: if process.stdout: - response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) + response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=5.0 + ) response_line = response_bytes.decode().strip() print(f"Response: {response_line}") @@ -85,7 +89,9 @@ async def test_mcp_communication(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for(process.stderr.read(4096), timeout=1.0) + stderr_data = await asyncio.wait_for( + process.stderr.read(4096), timeout=1.0 + ) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR: {stderr_text}") diff --git a/test_minimal_mcp.py b/test_minimal_mcp.py index c0ed104..551cdd2 100644 --- a/test_minimal_mcp.py +++ b/test_minimal_mcp.py @@ -83,7 +83,9 @@ async def hello() -> list[TextContent]: # Try to get response try: if process.stdout: - response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) + response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=5.0 + ) response_line = response_bytes.decode().strip() print(f"Response: {response_line}") diff --git a/test_module_startup.py b/test_module_startup.py index 3479129..9553f5e 100644 --- a/test_module_startup.py +++ b/test_module_startup.py @@ -71,7 +71,9 @@ async def test_module_startup(): try: if process.stdout: print("Waiting for response...") - response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=10.0) + response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=10.0 + ) response_line = response_bytes.decode().strip() print(f"✅ Response: {response_line}") @@ -88,7 +90,9 @@ async def test_module_startup(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for(process.stderr.read(2048), timeout=1.0) + stderr_data = await asyncio.wait_for( + process.stderr.read(2048), timeout=1.0 + ) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR during timeout: {stderr_text}") diff --git a/test_production_server.py b/test_production_server.py index cb4f4a6..88fe636 100644 --- a/test_production_server.py +++ b/test_production_server.py @@ -71,7 +71,9 @@ async def test_production_server(): try: if process.stdout: print("Waiting for response...") - response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=10.0) + response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=10.0 + ) response_line = response_bytes.decode().strip() print(f"✅ Response: {response_line}") @@ -94,7 +96,9 @@ async def test_production_server(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for(process.stderr.read(4096), timeout=1.0) + stderr_data = await asyncio.wait_for( + process.stderr.read(4096), timeout=1.0 + ) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR during timeout: {stderr_text}") diff --git a/test_sbctl_direct.py b/test_sbctl_direct.py index 962ff45..306186d 100644 --- a/test_sbctl_direct.py +++ b/test_sbctl_direct.py @@ -80,9 +80,13 @@ async def test_sbctl_direct(): print(f" Found kubeconfig files: {kubeconfig_files}") # Look for local-kubeconfig files - local_kubeconfig_files = list(location.glob("**/local-kubeconfig*")) + local_kubeconfig_files = list( + location.glob("**/local-kubeconfig*") + ) if local_kubeconfig_files: - print(f" Found local-kubeconfig files: {local_kubeconfig_files}") + print( + f" Found local-kubeconfig files: {local_kubeconfig_files}" + ) except Exception as search_e: print(f" Error searching {location}: {search_e}") diff --git a/test_simple_mcp.py b/test_simple_mcp.py index 68c165d..f9fee80 100644 --- a/test_simple_mcp.py +++ b/test_simple_mcp.py @@ -54,7 +54,9 @@ async def test_simple_mcp(): # Wait for any response at all try: if process.stdout: - response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=3.0) + response_bytes = await asyncio.wait_for( + process.stdout.readline(), timeout=3.0 + ) response_line = response_bytes.decode().strip() print(f"Got response: {response_line}") @@ -76,7 +78,9 @@ async def test_simple_mcp(): # Read stderr to see what's happening if process.stderr: try: - stderr_data = await asyncio.wait_for(process.stderr.read(1024), timeout=1.0) + stderr_data = await asyncio.wait_for( + process.stderr.read(1024), timeout=1.0 + ) if stderr_data: print(f"STDERR: {stderr_data.decode()}") except asyncio.TimeoutError: diff --git a/tests/conftest.py b/tests/conftest.py index a0dc6e8..761eb31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -88,7 +88,9 @@ def clean_asyncio(): # Only log specific understood errors to avoid silent failures import logging - logging.getLogger("tests").debug(f"Controlled exception during event loop cleanup: {e}") + logging.getLogger("tests").debug( + f"Controlled exception during event loop cleanup: {e}" + ) # Create a new event loop for the next test asyncio.set_event_loop(asyncio.new_event_loop()) @@ -225,7 +227,9 @@ def container_image(request): # Skip container builds in CI due to melange/apko limitations # Container builds are validated in the publish workflow if os.environ.get("CI") == "true": - pytest.skip("Container image builds are skipped in CI - run locally with 'pytest -m slow'") + pytest.skip( + "Container image builds are skipped in CI - run locally with 'pytest -m slow'" + ) # Get project root directory project_root = Path(__file__).parents[1] @@ -251,14 +255,22 @@ def container_image(request): # Explicitly clean up any running containers containers_result = subprocess.run( - ["podman", "ps", "-q", "--filter", "ancestor=troubleshoot-mcp-server-dev:latest"], + [ + "podman", + "ps", + "-q", + "--filter", + "ancestor=troubleshoot-mcp-server-dev:latest", + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10, check=False, ) - containers = containers_result.stdout.strip().split("\n") if containers_result.stdout else [] + containers = ( + containers_result.stdout.strip().split("\n") if containers_result.stdout else [] + ) for container_id in containers: if container_id: diff --git a/tests/e2e/test_build_reliability.py b/tests/e2e/test_build_reliability.py index ad3a2df..256d4c7 100644 --- a/tests/e2e/test_build_reliability.py +++ b/tests/e2e/test_build_reliability.py @@ -101,7 +101,9 @@ def test_container_build_never_uses_cached_configs(temp_project_dir): finally: # Clean up any test images - subprocess.run([runtime, "rmi", "-f", f"{image_name}:latest"], capture_output=True) + subprocess.run( + [runtime, "rmi", "-f", f"{image_name}:latest"], capture_output=True + ) def test_build_config_changes_reflected_in_tests(): @@ -118,19 +120,19 @@ def test_build_config_changes_reflected_in_tests(): conftest_content = conftest_path.read_text() # Verify the problematic caching code was removed - assert "Using existing container image for tests" not in conftest_content, ( - "The problematic caching logic should have been removed from conftest.py" - ) + assert ( + "Using existing container image for tests" not in conftest_content + ), "The problematic caching logic should have been removed from conftest.py" # Verify the fix is in place - assert "Building container image (Podman will use layer cache" in conftest_content, ( - "The fix to always build container images should be present in conftest.py" - ) + assert ( + "Building container image (Podman will use layer cache" in conftest_content + ), "The fix to always build container images should be present in conftest.py" # Verify we always build (no dangerous skip logic) - assert "Always run the build process" in conftest_content, ( - "Tests should always run build process and rely on Podman layer caching" - ) + assert ( + "Always run the build process" in conftest_content + ), "Tests should always run build process and rely on Podman layer caching" def test_sbctl_installation_path_is_correct(): @@ -147,14 +149,14 @@ def test_sbctl_installation_path_is_correct(): config_content = melange_config.read_text() # Verify the fix is in place - assert "${{targets.destdir}}/usr/bin" in config_content, ( - "sbctl should be installed to ${{targets.destdir}}/usr/bin for proper packaging" - ) + assert ( + "${{targets.destdir}}/usr/bin" in config_content + ), "sbctl should be installed to ${{targets.destdir}}/usr/bin for proper packaging" # Verify the broken pattern is not present - assert "/usr/local/bin/sbctl" not in config_content, ( - "sbctl should not be installed to /usr/local/bin (the broken path)" - ) + assert ( + "/usr/local/bin/sbctl" not in config_content + ), "sbctl should not be installed to /usr/local/bin (the broken path)" # Verify the installation command sequence is correct lines = config_content.split("\n") diff --git a/tests/e2e/test_container_bundle_validation.py b/tests/e2e/test_container_bundle_validation.py index d72cf88..caddd11 100644 --- a/tests/e2e/test_container_bundle_validation.py +++ b/tests/e2e/test_container_bundle_validation.py @@ -95,7 +95,9 @@ async def send_request(self, method: str, params: Optional[dict] = None) -> dict raise RuntimeError("Container stdout not available") try: - response_bytes = await asyncio.wait_for(self.process.stdout.readline(), timeout=60.0) + response_bytes = await asyncio.wait_for( + self.process.stdout.readline(), timeout=60.0 + ) response_line = response_bytes.decode().strip() logger.debug(f"Received: {response_line}") @@ -121,7 +123,9 @@ async def send_request(self, method: str, params: Optional[dict] = None) -> dict async def call_tool(self, tool_name: str, arguments: dict) -> dict: """Call an MCP tool in the container.""" - return await self.send_request("tools/call", {"name": tool_name, "arguments": arguments}) + return await self.send_request( + "tools/call", {"name": tool_name, "arguments": arguments} + ) async def stop(self) -> None: """Stop the container.""" @@ -340,13 +344,17 @@ async def test_container_complete_workflow( # Step 1: Initialize bundle bundle_path = f"/data/bundles/{test_bundle_in_dir.name}" - init_response = await client.call_tool("initialize_bundle", {"source": bundle_path}) + init_response = await client.call_tool( + "initialize_bundle", {"source": bundle_path} + ) assert "result" in init_response logger.info("✅ Bundle initialized in container") # Step 2: List files - files_response = await client.call_tool("list_files", {"path": "/", "recursive": False}) + files_response = await client.call_tool( + "list_files", {"path": "/", "recursive": False} + ) assert "result" in files_response files_content = files_response["result"]["content"][0]["text"] diff --git a/tests/e2e/test_container_production_validation.py b/tests/e2e/test_container_production_validation.py index b9ddd66..b840337 100644 --- a/tests/e2e/test_container_production_validation.py +++ b/tests/e2e/test_container_production_validation.py @@ -63,9 +63,9 @@ def test_container_has_required_tools_isolated(container_image: str): f"This indicates the tool is not properly packaged. " f"returncode: {result.returncode}, stdout: {result.stdout}, stderr: {result.stderr}" ) - assert "Usage:" in result.stdout or "usage:" in result.stdout, ( - f"sbctl --help output doesn't contain expected usage text: {result.stdout}" - ) + assert ( + "Usage:" in result.stdout or "usage:" in result.stdout + ), f"sbctl --help output doesn't contain expected usage text: {result.stdout}" elif tool_name == "kubectl": # Test kubectl exists and works @@ -93,9 +93,9 @@ def test_container_has_required_tools_isolated(container_image: str): f"This indicates the tool is not properly packaged. " f"returncode: {result.returncode}, stdout: {result.stdout}, stderr: {result.stderr}" ) - assert "Client Version:" in result.stdout, ( - f"kubectl version output doesn't contain expected version text: {result.stdout}" - ) + assert ( + "Client Version:" in result.stdout + ), f"kubectl version output doesn't contain expected version text: {result.stdout}" elif tool_name == "python3": # Test python3 exists and works @@ -122,9 +122,9 @@ def test_container_has_required_tools_isolated(container_image: str): f"This indicates the tool is not properly packaged. " f"returncode: {result.returncode}, stdout: {result.stdout}, stderr: {result.stderr}" ) - assert "Python" in result.stdout, ( - f"python3 --version output doesn't contain expected version text: {result.stdout}" - ) + assert ( + "Python" in result.stdout + ), f"python3 --version output doesn't contain expected version text: {result.stdout}" def test_container_bundle_initialization_isolated( @@ -330,4 +330,6 @@ def test_production_container_mcp_protocol(): assert "jsonrpc" in response, f"Invalid MCP response format: {response}" assert response.get("id") == "test-1", f"Response ID mismatch: {response}" except json.JSONDecodeError as e: - pytest.fail(f"Container returned invalid JSON response: {result.stdout}, error: {e}") + pytest.fail( + f"Container returned invalid JSON response: {result.stdout}, error: {e}" + ) diff --git a/tests/e2e/test_container_shutdown_reliability.py b/tests/e2e/test_container_shutdown_reliability.py index a1d9a10..589080a 100644 --- a/tests/e2e/test_container_shutdown_reliability.py +++ b/tests/e2e/test_container_shutdown_reliability.py @@ -135,7 +135,9 @@ def test_stdio_mode_sigterm_shutdown(self): ) # Should exit cleanly without Python runtime errors - assert "Fatal Python error" not in stderr, f"Python runtime error detected: {stderr}" + assert ( + "Fatal Python error" not in stderr + ), f"Python runtime error detected: {stderr}" assert "_enter_buffered_busy" not in stderr assert "could not acquire lock" not in stderr @@ -289,7 +291,9 @@ def test_container_like_environment_full_lifecycle(self): ) + "\n", # List tools - json.dumps({"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2}) + json.dumps( + {"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2} + ) + "\n", ] diff --git a/tests/e2e/test_direct_tool_integration.py b/tests/e2e/test_direct_tool_integration.py index 09ec909..0251a80 100644 --- a/tests/e2e/test_direct_tool_integration.py +++ b/tests/e2e/test_direct_tool_integration.py @@ -101,8 +101,12 @@ async def test_initialize_bundle_tool_direct(self, test_bundle_copy): try: result_data = json.loads(result_text) - assert "bundle_id" in result_data, f"Response should contain bundle_id: {result_text}" - assert "status" in result_data, f"Response should contain status: {result_text}" + assert ( + "bundle_id" in result_data + ), f"Response should contain bundle_id: {result_text}" + assert ( + "status" in result_data + ), f"Response should contain status: {result_text}" except json.JSONDecodeError: # If not JSON, check for success indicators in text assert any( @@ -124,16 +128,21 @@ async def test_list_available_bundles_tool_direct(self, test_bundle_copy): # The bundle should be listed since it exists in the storage directory # If not found, it might be a valid case where the bundle isn't recognized - if bundle_name not in bundles_text and "No support bundles found" in bundles_text: + if ( + bundle_name not in bundles_text + and "No support bundles found" in bundles_text + ): # This is acceptable - bundle might need to be in a specific format - print("Bundle not automatically detected, this is expected for test bundles") - assert "support bundles" in bundles_text.lower(), ( - f"Should mention bundles: {bundles_text}" + print( + "Bundle not automatically detected, this is expected for test bundles" ) + assert ( + "support bundles" in bundles_text.lower() + ), f"Should mention bundles: {bundles_text}" else: - assert bundle_name in bundles_text, ( - f"Bundle {bundle_name} should appear in list: {bundles_text}" - ) + assert ( + bundle_name in bundles_text + ), f"Bundle {bundle_name} should appear in list: {bundles_text}" @pytest.mark.asyncio async def test_file_operations_direct(self, test_bundle_copy): @@ -150,7 +159,9 @@ async def test_file_operations_direct(self, test_bundle_copy): files_text = list_content[0].text # Should have some files in the bundle - assert len(files_text.strip()) > 0, f"File listing should not be empty: {files_text}" + assert ( + len(files_text.strip()) > 0 + ), f"File listing should not be empty: {files_text}" # Look for a file to read (try common bundle file patterns) import json @@ -167,12 +178,14 @@ async def test_file_operations_direct(self, test_bundle_copy): if file_path: read_args = ReadFileArgs(path=file_path) read_content = await read_file(read_args) - assert len(read_content) > 0, f"Should be able to read file {file_path}" + assert ( + len(read_content) > 0 + ), f"Should be able to read file {file_path}" except json.JSONDecodeError: # If not JSON, just verify we got some text output - assert "file" in files_text.lower() or "directory" in files_text.lower(), ( - f"File listing should mention files or directories: {files_text}" - ) + assert ( + "file" in files_text.lower() or "directory" in files_text.lower() + ), f"File listing should mention files or directories: {files_text}" @pytest.mark.asyncio async def test_grep_functionality_direct(self, test_bundle_copy): @@ -205,10 +218,14 @@ async def test_kubectl_tool_direct(self, test_bundle_copy): await initialize_bundle(init_args) # Test kubectl version command (should work even with limited cluster) - kubectl_args = KubectlCommandArgs(command="version --client", timeout=10, json_output=False) + kubectl_args = KubectlCommandArgs( + command="version --client", timeout=10, json_output=False + ) try: - kubectl_content = await asyncio.wait_for(kubectl(kubectl_args), timeout=15.0) + kubectl_content = await asyncio.wait_for( + kubectl(kubectl_args), timeout=15.0 + ) assert len(kubectl_content) > 0, "Should have kubectl output" kubectl_text = kubectl_content[0].text diff --git a/tests/e2e/test_mcp_protocol_integration.py b/tests/e2e/test_mcp_protocol_integration.py index 93a24ce..dfa7255 100644 --- a/tests/e2e/test_mcp_protocol_integration.py +++ b/tests/e2e/test_mcp_protocol_integration.py @@ -39,7 +39,9 @@ def temp_bundle_dir(): class TestMCPProtocolLifecycle: """Test complete MCP server lifecycle via JSON-RPC protocol.""" - async def test_server_startup_and_initialization(self, temp_bundle_dir, test_bundle_path): + async def test_server_startup_and_initialization( + self, temp_bundle_dir, test_bundle_path + ): """ Test server startup and MCP initialization handshake. @@ -139,9 +141,9 @@ async def test_tool_discovery_via_protocol(self, temp_bundle_dir, test_bundle_pa } actual_tools = {tool["name"] for tool in tools} - assert expected_tools.issubset(actual_tools), ( - f"Missing expected tools. Expected: {expected_tools}, Actual: {actual_tools}" - ) + assert expected_tools.issubset( + actual_tools + ), f"Missing expected tools. Expected: {expected_tools}, Actual: {actual_tools}" # Verify each tool has required properties for tool in tools: @@ -171,26 +173,33 @@ async def test_bundle_loading_via_initialize_bundle_tool( await client.initialize_mcp() # Test bundle loading via MCP tool call - content = await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) + content = await client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ) # Verify successful bundle loading assert len(content) > 0, "initialize_bundle should return content" result_text = content[0].get("text", "") - assert "successfully" in result_text.lower() or "initialized" in result_text.lower(), ( - f"Bundle initialization appears to have failed. Response: {result_text}" - ) + assert ( + "successfully" in result_text.lower() + or "initialized" in result_text.lower() + ), f"Bundle initialization appears to have failed. Response: {result_text}" # Verify bundle is now accessible via list_available_bundles bundles_content = await client.call_tool("list_available_bundles") - assert len(bundles_content) > 0, "Should have at least one bundle after initialization" + assert ( + len(bundles_content) > 0 + ), "Should have at least one bundle after initialization" bundles_text = bundles_content[0].get("text", "") - assert bundle_name in bundles_text, ( - f"Loaded bundle {bundle_name} should appear in bundle list: {bundles_text}" - ) + assert ( + bundle_name in bundles_text + ), f"Loaded bundle {bundle_name} should appear in bundle list: {bundles_text}" - async def test_file_operations_via_protocol(self, temp_bundle_dir, test_bundle_path): + async def test_file_operations_via_protocol( + self, temp_bundle_dir, test_bundle_path + ): """ Test file operations (list_files, read_file) via MCP protocol. @@ -207,7 +216,9 @@ async def test_file_operations_via_protocol(self, temp_bundle_dir, test_bundle_p async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) + await client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ) # Test file listing via protocol files_content = await client.call_tool("list_files", {"path": "."}) @@ -223,11 +234,15 @@ async def test_file_operations_via_protocol(self, temp_bundle_dir, test_bundle_p # Extract file names from the listing files_text = files_list[0].get("text", "") - file_lines = [line.strip() for line in files_text.split("\n") if line.strip()] + file_lines = [ + line.strip() for line in files_text.split("\n") if line.strip() + ] assert len(file_lines) > 0, "Should have at least one file in the bundle" # Get the first file for testing (remove any tree symbols) - first_file = file_lines[0].split()[-1] # Take the last part after any tree symbols + first_file = file_lines[0].split()[ + -1 + ] # Take the last part after any tree symbols # Test reading the actual file that exists in the bundle file_content = await client.call_tool("read_file", {"path": first_file}) @@ -237,7 +252,9 @@ async def test_file_operations_via_protocol(self, temp_bundle_dir, test_bundle_p # Some files might be binary or empty, just verify we got a response assert content_text is not None, "File content should be retrievable" - async def test_grep_functionality_via_protocol(self, temp_bundle_dir, test_bundle_path): + async def test_grep_functionality_via_protocol( + self, temp_bundle_dir, test_bundle_path + ): """ Test file searching via the grep_files MCP tool. @@ -253,11 +270,15 @@ async def test_grep_functionality_via_protocol(self, temp_bundle_dir, test_bundl async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) + await client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ) # Test grep functionality via protocol # Search for a common term that should exist in Kubernetes bundles - grep_content = await client.call_tool("grep_files", {"pattern": "kind:", "path": "."}) + grep_content = await client.call_tool( + "grep_files", {"pattern": "kind:", "path": "."} + ) assert len(grep_content) > 0, "grep_files should return content" @@ -281,10 +302,14 @@ async def test_kubectl_tool_via_protocol(self, temp_bundle_dir, test_bundle_path async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) + await client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ) # Test basic kubectl command via protocol - kubectl_content = await client.call_tool("kubectl", {"command": "get nodes"}) + kubectl_content = await client.call_tool( + "kubectl", {"command": "get nodes"} + ) assert len(kubectl_content) > 0, "kubectl should return content" @@ -294,7 +319,9 @@ async def test_kubectl_tool_via_protocol(self, temp_bundle_dir, test_bundle_path # The command might fail (no nodes in test bundle), but should not crash # We just verify the protocol layer works correctly - async def test_kubectl_exec_handling_via_protocol(self, temp_bundle_dir, test_bundle_path): + async def test_kubectl_exec_handling_via_protocol( + self, temp_bundle_dir, test_bundle_path + ): """ Test kubectl exec command handling via MCP protocol. @@ -311,17 +338,23 @@ async def test_kubectl_exec_handling_via_protocol(self, temp_bundle_dir, test_bu async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) + await client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ) # Test kubectl exec command via protocol - this should not crash the server kubectl_content = await client.call_tool( "kubectl", {"command": "exec -it some-pod -- /bin/bash"} ) - assert len(kubectl_content) > 0, "kubectl exec should return content (even if error)" + assert ( + len(kubectl_content) > 0 + ), "kubectl exec should return content (even if error)" kubectl_text = kubectl_content[0].get("text", "") - assert isinstance(kubectl_text, str), "kubectl exec result should be a string" + assert isinstance( + kubectl_text, str + ), "kubectl exec result should be a string" # The command will likely fail, but should return a sensible error message # and not crash the server. The key is that the server doesn't crash @@ -329,18 +362,20 @@ async def test_kubectl_exec_handling_via_protocol(self, temp_bundle_dir, test_bu # It's OK if kubectl exec fails - the important thing is it doesn't crash # and returns a meaningful response - assert len(kubectl_text.strip()) > 0, ( - "kubectl exec should return some response, even if it's an error message" - ) + assert ( + len(kubectl_text.strip()) > 0 + ), "kubectl exec should return some response, even if it's an error message" # Verify server is still responsive after kubectl exec # by making another tool call tools_response = await client.send_request("tools/list") - assert "result" in tools_response, ( - "Server should still be responsive after kubectl exec" - ) + assert ( + "result" in tools_response + ), "Server should still be responsive after kubectl exec" - async def test_kubectl_interactive_commands_handling(self, temp_bundle_dir, test_bundle_path): + async def test_kubectl_interactive_commands_handling( + self, temp_bundle_dir, test_bundle_path + ): """ Test that interactive kubectl commands are handled gracefully. @@ -360,7 +395,9 @@ async def test_kubectl_interactive_commands_handling(self, temp_bundle_dir, test async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) + await client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ) # Test various potentially problematic kubectl commands problematic_commands = [ @@ -377,14 +414,18 @@ async def test_kubectl_interactive_commands_handling(self, temp_bundle_dir, test assert len(kubectl_content) > 0, f"kubectl {cmd} should return content" kubectl_text = kubectl_content[0].get("text", "") - assert isinstance(kubectl_text, str), f"kubectl {cmd} result should be a string" - assert len(kubectl_text.strip()) > 0, f"kubectl {cmd} should return some response" + assert isinstance( + kubectl_text, str + ), f"kubectl {cmd} result should be a string" + assert ( + len(kubectl_text.strip()) > 0 + ), f"kubectl {cmd} should return some response" # Verify server is still responsive after each command tools_response = await client.send_request("tools/list") - assert "result" in tools_response, ( - f"Server should be responsive after kubectl {cmd}" - ) + assert ( + "result" in tools_response + ), f"Server should be responsive after kubectl {cmd}" class TestMCPProtocolErrorHandling: @@ -414,15 +455,17 @@ async def test_bundle_loading_failure_via_protocol(self, temp_bundle_dir): # If it doesn't throw, check that error is reported in content assert len(content) > 0, "Should return error content" result_text = content[0].get("text", "") - assert "error" in result_text.lower() or "not found" in result_text.lower(), ( - f"Should report error for non-existent bundle: {result_text}" - ) + assert ( + "error" in result_text.lower() or "not found" in result_text.lower() + ), f"Should report error for non-existent bundle: {result_text}" except RuntimeError as e: # It's also acceptable for this to raise an RPC error assert "error" in str(e).lower(), f"Error should be descriptive: {e}" - async def test_file_access_error_via_protocol(self, temp_bundle_dir, test_bundle_path): + async def test_file_access_error_via_protocol( + self, temp_bundle_dir, test_bundle_path + ): """ Test file access error handling via MCP protocol. @@ -439,7 +482,9 @@ async def test_file_access_error_via_protocol(self, temp_bundle_dir, test_bundle async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) + await client.call_tool( + "initialize_bundle", {"source": str(test_bundle_copy)} + ) # Try to read non-existent file try: @@ -450,15 +495,16 @@ async def test_file_access_error_via_protocol(self, temp_bundle_dir, test_bundle # Should either throw or return error in content if len(content) > 0: result_text = content[0].get("text", "") - assert "error" in result_text.lower() or "not found" in result_text.lower(), ( - f"Should report error for non-existent file: {result_text}" - ) + assert ( + "error" in result_text.lower() + or "not found" in result_text.lower() + ), f"Should report error for non-existent file: {result_text}" except RuntimeError as e: # It's also acceptable for this to raise an RPC error - assert "error" in str(e).lower() or "not found" in str(e).lower(), ( - f"Error should be descriptive: {e}" - ) + assert ( + "error" in str(e).lower() or "not found" in str(e).lower() + ), f"Error should be descriptive: {e}" async def test_invalid_tool_call_via_protocol(self, temp_bundle_dir): """ @@ -475,7 +521,9 @@ async def test_invalid_tool_call_via_protocol(self, temp_bundle_dir): # Try to call non-existent tool try: - response = await client.send_request("tools/call", {"name": "nonexistent_tool"}) + response = await client.send_request( + "tools/call", {"name": "nonexistent_tool"} + ) # Should not reach here - should get an error response pytest.fail(f"Expected error for non-existent tool, got: {response}") @@ -514,7 +562,9 @@ async def test_protocol_robustness(self, temp_bundle_dir): class TestMCPProtocolCompleteWorkflow: """Test complete workflow combining all MCP tools via protocol.""" - async def test_complete_bundle_analysis_workflow(self, temp_bundle_dir, test_bundle_path): + async def test_complete_bundle_analysis_workflow( + self, temp_bundle_dir, test_bundle_path + ): """ Test complete bundle analysis workflow via MCP protocol. @@ -561,7 +611,9 @@ async def test_complete_bundle_analysis_workflow(self, temp_bundle_dir, test_bun assert len(grep_result) > 0 # Step 6: Try kubectl command - kubectl_result = await client.call_tool("kubectl", {"command": "version --client"}) + kubectl_result = await client.call_tool( + "kubectl", {"command": "version --client"} + ) assert len(kubectl_result) > 0 # All steps completed successfully via MCP protocol diff --git a/tests/e2e/test_non_container.py b/tests/e2e/test_non_container.py index 572e663..ed54491 100644 --- a/tests/e2e/test_non_container.py +++ b/tests/e2e/test_non_container.py @@ -26,7 +26,9 @@ def test_cli_module_exists(): try: from mcp_server_troubleshoot import cli - assert callable(getattr(cli, "main", None)), "CLI module does not have a main function" + assert callable( + getattr(cli, "main", None) + ), "CLI module does not have a main function" except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.cli module") @@ -36,7 +38,9 @@ def test_bundle_module_exists(): try: from mcp_server_troubleshoot import bundle - assert hasattr(bundle, "BundleManager"), "Bundle module does not have BundleManager class" + assert hasattr( + bundle, "BundleManager" + ), "Bundle module does not have BundleManager class" except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.bundle module") @@ -46,7 +50,9 @@ def test_files_module_exists(): try: from mcp_server_troubleshoot import files - assert hasattr(files, "FileExplorer"), "Files module does not have FileExplorer class" + assert hasattr( + files, "FileExplorer" + ), "Files module does not have FileExplorer class" except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.files module") @@ -56,9 +62,9 @@ def test_kubectl_module_exists(): try: from mcp_server_troubleshoot import kubectl - assert hasattr(kubectl, "KubectlExecutor"), ( - "Kubectl module does not have KubectlExecutor class" - ) + assert hasattr( + kubectl, "KubectlExecutor" + ), "Kubectl module does not have KubectlExecutor class" except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.kubectl module") @@ -84,12 +90,12 @@ def test_configuration_loading(): "log_level": "INFO", } # Test we can load configuration functions - assert hasattr(config, "get_recommended_client_config"), ( - "Config module missing get_recommended_client_config" - ) - assert hasattr(config, "load_config_from_path"), ( - "Config module missing load_config_from_path" - ) + assert hasattr( + config, "get_recommended_client_config" + ), "Config module missing get_recommended_client_config" + assert hasattr( + config, "load_config_from_path" + ), "Config module missing load_config_from_path" # Verify the test config values are as expected bundle_storage = test_config["bundle_storage"] @@ -123,9 +129,9 @@ def test_version_command(): check=False, ) assert result.returncode == 0, f"Version command failed with: {result.stderr}" - assert "version" in result.stdout.lower() or "version" in result.stderr.lower(), ( - "Version information not found in output" - ) + assert ( + "version" in result.stdout.lower() or "version" in result.stderr.lower() + ), "Version information not found in output" @pytest.mark.asyncio diff --git a/tests/e2e/test_podman.py b/tests/e2e/test_podman.py index c5518e4..20b7095 100644 --- a/tests/e2e/test_podman.py +++ b/tests/e2e/test_podman.py @@ -106,7 +106,9 @@ def test_podman_availability() -> None: print(f"Using Podman version: {result.stdout.strip()}") -def test_basic_podman_run(container_image: str, container_name: str, temp_bundle_dir: Path) -> None: +def test_basic_podman_run( + container_image: str, container_name: str, temp_bundle_dir: Path +) -> None: """Test that the Podman container runs and exits successfully.""" result = subprocess.run( [ @@ -168,7 +170,9 @@ def test_installed_tools(container_image: str, container_name: str) -> None: assert result.stdout.strip(), f"{tool} path is empty" -def test_help_command(container_image: str, container_name: str, temp_bundle_dir: Path) -> None: +def test_help_command( + container_image: str, container_name: str, temp_bundle_dir: Path +) -> None: """Test that the application's help command works.""" result = subprocess.run( [ @@ -197,7 +201,9 @@ def test_help_command(container_image: str, container_name: str, temp_bundle_dir assert "usage:" in combined_output.lower(), "Application help command failed" -def test_version_command(container_image: str, container_name: str, temp_bundle_dir: Path) -> None: +def test_version_command( + container_image: str, container_name: str, temp_bundle_dir: Path +) -> None: """Test that the application's version command works.""" result = subprocess.run( [ @@ -266,7 +272,9 @@ def test_process_dummy_bundle( # Verify the application CLI works assert result.returncode == 0, f"Failed to run container: {result.stderr}" - assert "usage:" in (result.stdout + result.stderr).lower(), "Application CLI is not working" + assert ( + "usage:" in (result.stdout + result.stderr).lower() + ), "Application CLI is not working" if __name__ == "__main__": diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 4ef97c7..7e35dcd 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -50,8 +50,12 @@ def assert_api_response_valid( """ assert isinstance(response, list), "Response should be a list" assert len(response) > 0, "Response should not be empty" - assert hasattr(response[0], "type"), "Response item should have 'type' attribute" - assert response[0].type == expected_type, f"Response type should be '{expected_type}'" + assert hasattr( + response[0], "type" + ), "Response item should have 'type' attribute" + assert ( + response[0].type == expected_type + ), f"Response type should be '{expected_type}'" if contains and hasattr(response[0], "text"): for text in contains: @@ -72,9 +76,9 @@ def assert_object_matches_attrs(obj: Any, expected_attrs: Dict[str, Any]) -> Non for attr, expected in expected_attrs.items(): assert hasattr(obj, attr), f"Object should have attribute '{attr}'" actual = getattr(obj, attr) - assert actual == expected, ( - f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" - ) + assert ( + actual == expected + ), f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" @staticmethod async def assert_asyncio_timeout(coro, timeout: float = 0.1) -> None: diff --git a/tests/integration/mcp_test_utils.py b/tests/integration/mcp_test_utils.py index d882254..faade11 100644 --- a/tests/integration/mcp_test_utils.py +++ b/tests/integration/mcp_test_utils.py @@ -25,7 +25,9 @@ class MCPTestClient: with it using the JSON-RPC 2.0 protocol over stdio transport. """ - def __init__(self, bundle_dir: Optional[Path] = None, env: Optional[Dict[str, str]] = None): + def __init__( + self, bundle_dir: Optional[Path] = None, env: Optional[Dict[str, str]] = None + ): """ Initialize the MCP test client. @@ -200,7 +202,9 @@ async def send_request( raise RuntimeError(f"Response is not a JSON object: {response}") if response.get("jsonrpc") != "2.0": - raise RuntimeError(f"Invalid JSON-RPC version: {response.get('jsonrpc')}") + raise RuntimeError( + f"Invalid JSON-RPC version: {response.get('jsonrpc')}" + ) if response.get("id") != request_id: raise RuntimeError( @@ -221,7 +225,9 @@ async def send_request( logger.error(f"Error during RPC communication: {e}") raise - async def send_notification(self, method: str, params: Optional[Dict[str, Any]] = None) -> None: + async def send_notification( + self, method: str, params: Optional[Dict[str, Any]] = None + ) -> None: """ Send JSON-RPC notification (no response expected). @@ -259,7 +265,9 @@ async def send_notification(self, method: str, params: Optional[Dict[str, Any]] logger.error(f"Error sending notification: {e}") raise - async def initialize_mcp(self, client_info: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def initialize_mcp( + self, client_info: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: """ Send MCP initialize request to establish connection. @@ -272,7 +280,8 @@ async def initialize_mcp(self, client_info: Optional[Dict[str, Any]] = None) -> params = { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, - "clientInfo": client_info or {"name": "mcp-test-client", "version": "1.0.0"}, + "clientInfo": client_info + or {"name": "mcp-test-client", "version": "1.0.0"}, } response = await self.send_request("initialize", params) diff --git a/tests/integration/test_api_server_lifecycle.py b/tests/integration/test_api_server_lifecycle.py index 3db57f6..f8dd5d1 100644 --- a/tests/integration/test_api_server_lifecycle.py +++ b/tests/integration/test_api_server_lifecycle.py @@ -33,7 +33,9 @@ async def bundle_manager(self): def test_bundle_path(self): """Path to test bundle fixture.""" return ( - Path(__file__).parent.parent / "fixtures" / "support-bundle-2025-04-11T14_05_31.tar.gz" + Path(__file__).parent.parent + / "fixtures" + / "support-bundle-2025-04-11T14_05_31.tar.gz" ) @pytest.mark.asyncio @@ -103,9 +105,9 @@ async def test_api_server_availability_check( result = await kubectl_executor.execute("get namespaces", json_output=True) # Verify we got a successful response - assert result.exit_code == 0, ( - f"kubectl command failed with exit code {result.exit_code}: {result.stderr}" - ) + assert ( + result.exit_code == 0 + ), f"kubectl command failed with exit code {result.exit_code}: {result.stderr}" # Verify we got valid JSON output assert result.is_json, "Expected JSON output from kubectl get namespaces" @@ -195,14 +197,19 @@ async def test_cleanup_verification( assert bundle_manager.sbctl_process is None # Verify bundle directory is cleaned up (if in temp directory) - if "/tmp" in str(initial_bundle_path) or "temp" in str(initial_bundle_path).lower(): + if ( + "/tmp" in str(initial_bundle_path) + or "temp" in str(initial_bundle_path).lower() + ): assert not initial_bundle_path.exists() # Verify process is terminated if initial_process_pid: try: os.kill(initial_process_pid, 0) - pytest.fail(f"Process {initial_process_pid} should have been terminated") + pytest.fail( + f"Process {initial_process_pid} should have been terminated" + ) except OSError: # Process is gone, which is expected pass @@ -212,7 +219,9 @@ async def test_cleanup_verification( pid_files = list(temp_dir.glob("**/mock_sbctl.pid")) # Filter to only our test files relevant_pid_files = [f for f in pid_files if "troubleshoot" in str(f.parent)] - assert len(relevant_pid_files) == 0, f"Found leftover PID files: {relevant_pid_files}" + assert ( + len(relevant_pid_files) == 0 + ), f"Found leftover PID files: {relevant_pid_files}" @pytest.mark.asyncio @pytest.mark.integration @@ -235,12 +244,16 @@ async def test_multiple_initialization_cleanup( # First initialization result1 = await bundle_manager.initialize_bundle(str(test_bundle_path)) assert result1.initialized is True - first_pid = bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None + first_pid = ( + bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None + ) # Second initialization should clean up first result2 = await bundle_manager.initialize_bundle(str(test_bundle_path)) assert result2.initialized is True - second_pid = bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None + second_pid = ( + bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None + ) # Verify new process is different (or at least that old one is gone) if first_pid and second_pid: @@ -248,12 +261,16 @@ async def test_multiple_initialization_cleanup( # Different PIDs - verify first process is gone try: os.kill(first_pid, 0) - pytest.fail(f"First process {first_pid} should have been terminated") + pytest.fail( + f"First process {first_pid} should have been terminated" + ) except OSError: # Expected - process should be gone pass - async def _collect_diagnostics(self, bundle_manager: BundleManager) -> Dict[str, Any]: + async def _collect_diagnostics( + self, bundle_manager: BundleManager + ) -> Dict[str, Any]: """Collect diagnostic information from the running system.""" diagnostics = {} @@ -328,7 +345,8 @@ async def _collect_diagnostics(self, bundle_manager: BundleManager) -> Dict[str, "diagnostic_collected_at": time.time(), "bundle_initialized_at": ( bundle_manager.active_bundle.path.stat().st_mtime - if bundle_manager.active_bundle and bundle_manager.active_bundle.path.exists() + if bundle_manager.active_bundle + and bundle_manager.active_bundle.path.exists() else None ), } diff --git a/tests/integration/test_mcp_protocol_errors.py b/tests/integration/test_mcp_protocol_errors.py index c46f21e..3f917b8 100644 --- a/tests/integration/test_mcp_protocol_errors.py +++ b/tests/integration/test_mcp_protocol_errors.py @@ -146,7 +146,9 @@ async def test_rapid_initialization_requests(self, mcp_client): ) # Either some succeed or all fail gracefully - assert valid_responses >= 0 # This will always pass but documents the expectation + assert ( + valid_responses >= 0 + ) # This will always pass but documents the expectation except Exception as e: # Rapid requests may cause issues - this is acceptable for robustness testing diff --git a/tests/integration/test_real_bundle.py b/tests/integration/test_real_bundle.py index d44be03..d5a3856 100644 --- a/tests/integration/test_real_bundle.py +++ b/tests/integration/test_real_bundle.py @@ -74,17 +74,23 @@ def test_sbctl_help_behavior(test_support_bundle): # Verify sbctl is available (basic behavior) # Log which sbctl is being used for debugging result = subprocess.run(["which", "sbctl"], capture_output=True, text=True) - assert result.returncode == 0, "sbctl command should be available (which sbctl failed)" + assert ( + result.returncode == 0 + ), "sbctl command should be available (which sbctl failed)" print(f"Using sbctl at: {result.stdout.strip()}") # Also check if executable permission is set sbctl_path = result.stdout.strip() if sbctl_path: - perm_result = subprocess.run(["ls", "-la", sbctl_path], capture_output=True, text=True) + perm_result = subprocess.run( + ["ls", "-la", sbctl_path], capture_output=True, text=True + ) print(f"sbctl permissions: {perm_result.stdout.strip()}") # Check help output behavior - help_result = subprocess.run(["sbctl", "--help"], capture_output=True, text=True, timeout=5) + help_result = subprocess.run( + ["sbctl", "--help"], capture_output=True, text=True, timeout=5 + ) # Verify the command ran successfully assert help_result.returncode == 0, "sbctl help command should succeed" @@ -121,13 +127,15 @@ def test_sbctl_help_behavior(test_support_bundle): ) # Verify help for serve is available - assert serve_help_result.returncode == 0, "sbctl serve help command should succeed" + assert ( + serve_help_result.returncode == 0 + ), "sbctl serve help command should succeed" # Verify serve help contains expected options serve_help_output = serve_help_result.stdout - assert "--support-bundle-location" in serve_help_output, ( - "Serve command should document bundle location option" - ) + assert ( + "--support-bundle-location" in serve_help_output + ), "Serve command should document bundle location option" @pytest.mark.asyncio @@ -146,7 +154,9 @@ async def test_bundle_lifecycle(bundle_manager_fixture): manager, real_bundle_path = bundle_manager_fixture # Act: Initialize the bundle - result = await asyncio.wait_for(manager.initialize_bundle(str(real_bundle_path)), timeout=30.0) + result = await asyncio.wait_for( + manager.initialize_bundle(str(real_bundle_path)), timeout=30.0 + ) # Assert: Verify functional behavior (not implementation details) assert result.initialized, "Bundle should be marked as initialized" @@ -156,7 +166,9 @@ async def test_bundle_lifecycle(bundle_manager_fixture): # Verify the bundle can be retrieved by the public API active_bundle = manager.get_active_bundle() assert active_bundle is not None, "Active bundle should be available" - assert active_bundle.id == result.id, "Active bundle should match initialized bundle" + assert ( + active_bundle.id == result.id + ), "Active bundle should match initialized bundle" # Verify API server functionality (behavior, not implementation) await manager.check_api_server_available() @@ -165,9 +177,9 @@ async def test_bundle_lifecycle(bundle_manager_fixture): # Test that re-initialization without force returns the same bundle second_result = await manager.initialize_bundle(str(real_bundle_path), force=False) - assert second_result.id == result.id, ( - "Re-initialization without force should return the same bundle" - ) + assert ( + second_result.id == result.id + ), "Re-initialization without force should return the same bundle" # Test that force re-initialization creates a new bundle force_result = await manager.initialize_bundle(str(real_bundle_path), force=True) @@ -214,14 +226,18 @@ async def test_bundle_initialization_workflow(bundle_manager_fixture, test_asser dir_contents = await explorer.list_files(first_dir, False) # Verify behavior - can list contents of subdirectory - assert dir_contents is not None, "Should be able to list subdirectory contents" - assert isinstance(dir_contents.entries, list), "Directory contents should be a list" + assert ( + dir_contents is not None + ), "Should be able to list subdirectory contents" + assert isinstance( + dir_contents.entries, list + ), "Directory contents should be a list" # TEST 3: Recursive listing behavior recursive_list = await explorer.list_files(first_dir, True) - assert recursive_list.total_files + recursive_list.total_dirs > 0, ( - "Recursive listing should find files/dirs" - ) + assert ( + recursive_list.total_files + recursive_list.total_dirs > 0 + ), "Recursive listing should find files/dirs" # TEST 4: File reading behavior # Find a file to read (we don't care which, just that we can read one) @@ -233,14 +249,18 @@ async def test_bundle_initialization_workflow(bundle_manager_fixture, test_asser # Verify behavior - can read file contents assert file_content is not None, "Should be able to read file contents" - assert file_content.content is not None, "File content should not be None" + assert ( + file_content.content is not None + ), "File content should not be None" # Check metadata (behavior we depend on) - assert file_content.path == first_file, "File content should have correct path" + assert ( + file_content.path == first_file + ), "File content should have correct path" # Note: We're checking for path existence, not name which might not be in all versions - assert hasattr(file_content, "content"), ( - "File content should have content attribute" - ) + assert hasattr( + file_content, "content" + ), "File content should have content attribute" @pytest.mark.asyncio @@ -273,17 +293,21 @@ async def test_bundle_manager_performance(bundle_manager_fixture): # Verify expected initialization behavior assert result.initialized, "Bundle should be marked as initialized" - assert result.kubeconfig_path.exists(), "Initialization should create a kubeconfig file" - assert duration < 45.0, ( - f"Initialization should complete in reasonable time (took {duration:.2f}s)" - ) + assert ( + result.kubeconfig_path.exists() + ), "Initialization should create a kubeconfig file" + assert ( + duration < 45.0 + ), f"Initialization should complete in reasonable time (took {duration:.2f}s)" # BEHAVIOR TEST 2: Verify kubeconfig has valid structure with open(result.kubeconfig_path, "r") as f: kubeconfig_content = f.read() # Check for essential kubeconfig fields that users and code depend on - assert "clusters" in kubeconfig_content, "Kubeconfig should contain clusters section" + assert ( + "clusters" in kubeconfig_content + ), "Kubeconfig should contain clusters section" assert "apiVersion" in kubeconfig_content, "Kubeconfig should contain API version" # BEHAVIOR TEST 3: The API server connection is attempted (we don't assert success @@ -294,18 +318,28 @@ async def test_bundle_manager_performance(bundle_manager_fixture): # This tests the observable behavior that getting the active bundle works active_bundle = manager.get_active_bundle() assert active_bundle is not None, "Manager should have an active bundle" - assert active_bundle.id == result.id, "Active bundle should match initialized bundle" + assert ( + active_bundle.id == result.id + ), "Active bundle should match initialized bundle" # BEHAVIOR TEST 5: Test diagnostic info is available - behavior users depend on diagnostics = await manager.get_diagnostic_info() - assert isinstance(diagnostics, dict), "Diagnostic info should be available as a dictionary" - assert "api_server_available" in diagnostics, "Diagnostics should report API server status" - assert "bundle_initialized" in diagnostics, "Diagnostics should report bundle status" + assert isinstance( + diagnostics, dict + ), "Diagnostic info should be available as a dictionary" + assert ( + "api_server_available" in diagnostics + ), "Diagnostics should report API server status" + assert ( + "bundle_initialized" in diagnostics + ), "Diagnostics should report bundle status" # Verify sbctl process was created successfully try: # Use ps to check for sbctl processes associated with this bundle - ps_result = subprocess.run(["ps", "-ef"], capture_output=True, text=True, timeout=5) + ps_result = subprocess.run( + ["ps", "-ef"], capture_output=True, text=True, timeout=5 + ) # There should be a sbctl process running for this bundle # We're checking behavior (process exists) not implementation (specific process args) diff --git a/tests/integration/test_server_lifecycle.py b/tests/integration/test_server_lifecycle.py index b7f26af..e7978df 100644 --- a/tests/integration/test_server_lifecycle.py +++ b/tests/integration/test_server_lifecycle.py @@ -88,7 +88,9 @@ async def test_server_startup_sequence_success(self, tmp_path: Path, mock_server os.environ.pop("MCP_BUNDLE_STORAGE", None) @pytest.mark.asyncio - async def test_server_startup_with_bundle_directory_scanning(self, tmp_path: Path, mock_server): + async def test_server_startup_with_bundle_directory_scanning( + self, tmp_path: Path, mock_server + ): """Test server startup automatically scans bundle directory for available bundles.""" bundle_dir = tmp_path / "bundles" bundle_dir.mkdir() @@ -151,7 +153,9 @@ async def test_server_startup_no_bundles_directory(self, mock_server): os.environ.pop("MCP_BUNDLE_STORAGE", None) @pytest.mark.asyncio - async def test_server_startup_invalid_bundles_handling(self, tmp_path: Path, mock_server): + async def test_server_startup_invalid_bundles_handling( + self, tmp_path: Path, mock_server + ): """Test server startup with invalid bundles (should handle errors gracefully).""" bundle_dir = tmp_path / "bundles" bundle_dir.mkdir() @@ -183,7 +187,9 @@ async def test_server_startup_invalid_bundles_handling(self, tmp_path: Path, moc assert len(valid_bundles) == 1 # Test including invalid bundles - all_bundles = await bundle_manager.list_available_bundles(include_invalid=True) + all_bundles = await bundle_manager.list_available_bundles( + include_invalid=True + ) assert len(all_bundles) >= len(bundles) finally: @@ -292,7 +298,9 @@ def bundle_manager_context(self, tmp_path: Path): return bundle_dir @pytest.mark.asyncio - async def test_automatic_bundle_discovery_on_startup(self, bundle_manager_context: Path): + async def test_automatic_bundle_discovery_on_startup( + self, bundle_manager_context: Path + ): """Test automatic bundle discovery when server starts.""" # Create multiple bundles bundles_created = [] @@ -368,7 +376,9 @@ async def test_periodic_bundle_cleanup_function(self, tmp_path: Path): bundle_manager = BundleManager(bundle_dir) # Create cleanup task with short interval - cleanup_task = asyncio.create_task(periodic_bundle_cleanup(bundle_manager, interval=0.1)) + cleanup_task = asyncio.create_task( + periodic_bundle_cleanup(bundle_manager, interval=0.1) + ) # Let it run briefly await asyncio.sleep(0.3) diff --git a/tests/integration/test_shutdown_race_condition.py b/tests/integration/test_shutdown_race_condition.py index 64220fb..029251c 100644 --- a/tests/integration/test_shutdown_race_condition.py +++ b/tests/integration/test_shutdown_race_condition.py @@ -123,12 +123,16 @@ async def main_loop(): # The test "passes" if it reproduces the race condition # (which means the bug exists and needs to be fixed) - race_condition_found = any(indicator in stderr for indicator in race_condition_indicators) + race_condition_found = any( + indicator in stderr for indicator in race_condition_indicators + ) # The test now verifies that the race condition is FIXED if race_condition_found: # If we see the race condition, the fix didn't work - pytest.fail(f"Race condition still present! Fix didn't work.\nstderr output:\n{stderr}") + pytest.fail( + f"Race condition still present! Fix didn't work.\nstderr output:\n{stderr}" + ) else: # Good! No race condition detected print("No race condition detected - fix is working!") diff --git a/tests/integration/test_signal_handling_integration.py b/tests/integration/test_signal_handling_integration.py index 31c0257..b4ba491 100644 --- a/tests/integration/test_signal_handling_integration.py +++ b/tests/integration/test_signal_handling_integration.py @@ -151,7 +151,9 @@ def test_sigterm_clean_shutdown(self): # Check what happened if return_code == -1: print(f"Process timed out. stderr:\n{stderr}") - pytest.fail("Process timed out, likely the signal handler did not properly exit") + pytest.fail( + "Process timed out, likely the signal handler did not properly exit" + ) # Should exit cleanly with code 0 or -15 (SIGTERM on Linux) assert return_code in ( @@ -322,9 +324,9 @@ async def main(): ) # Should not crash with Python runtime error - assert "Fatal Python error" not in stderr, ( - f"Race condition detected on iteration {i + 1}" - ) + assert ( + "Fatal Python error" not in stderr + ), f"Race condition detected on iteration {i + 1}" assert "_enter_buffered_busy" not in stderr def test_signal_with_resource_cleanup(self): diff --git a/tests/integration/test_subprocess_utilities_integration.py b/tests/integration/test_subprocess_utilities_integration.py index c1b3c52..14666e9 100644 --- a/tests/integration/test_subprocess_utilities_integration.py +++ b/tests/integration/test_subprocess_utilities_integration.py @@ -33,7 +33,9 @@ async def test_subprocess_exec_with_cleanup_basic_command(): async def test_subprocess_exec_with_cleanup_timeout_handling(): """Test that subprocess_exec_with_cleanup properly handles timeouts.""" with pytest.raises(asyncio.TimeoutError): - await subprocess_exec_with_cleanup("sleep", "10", timeout=0.1) # Very short timeout + await subprocess_exec_with_cleanup( + "sleep", "10", timeout=0.1 + ) # Very short timeout # If we get here, the timeout was handled properly and cleanup occurred @@ -80,7 +82,9 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.01) # Verify no transport warnings occurred - assert len(warnings_captured) == 0, f"Transport warnings detected: {warnings_captured}" + assert ( + len(warnings_captured) == 0 + ), f"Transport warnings detected: {warnings_captured}" finally: # Restore original warning handler @@ -106,7 +110,9 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): "echo", f"test-{i}", timeout=5.0 ) assert returncode == 0, f"Command {i} should succeed" - assert stdout == f"test-{i}\n".encode(), f"Should get expected output for {i}" + assert ( + stdout == f"test-{i}\n".encode() + ), f"Should get expected output for {i}" # Force garbage collection to trigger any transport issues for _ in range(5): @@ -114,7 +120,9 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.01) # Verify no transport warnings occurred - assert len(warnings_captured) == 0, f"Transport warnings detected: {warnings_captured}" + assert ( + len(warnings_captured) == 0 + ), f"Transport warnings detected: {warnings_captured}" finally: warnings.showwarning = original_showwarning @@ -192,7 +200,9 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.01) # Verify no transport cleanup warnings - assert len(warnings_captured) == 0, f"Transport warnings detected: {warnings_captured}" + assert ( + len(warnings_captured) == 0 + ), f"Transport warnings detected: {warnings_captured}" finally: warnings.showwarning = original_showwarning diff --git a/tests/integration/test_tool_functions.py b/tests/integration/test_tool_functions.py index bfa82ff..220e175 100644 --- a/tests/integration/test_tool_functions.py +++ b/tests/integration/test_tool_functions.py @@ -84,7 +84,9 @@ async def test_list_available_bundles_function(bundle_storage_dir): content_item = result[0] assert content_item.type == "text", "Content should be text type" - assert "No support bundles found" in content_item.text, "Should indicate no bundles found" + assert ( + "No support bundles found" in content_item.text + ), "Should indicate no bundles found" @pytest.mark.asyncio @@ -148,7 +150,9 @@ async def test_initialize_bundle_function_force_flag(bundle_storage_dir): assert len(result1) > 0, "First initialization should succeed" response1_text = result1[0].text - assert "Bundle initialized" in response1_text, "First initialization should report success" + assert ( + "Bundle initialized" in response1_text + ), "First initialization should report success" # Second initialization with force=True should also work args2 = InitializeBundleArgs(source=str(test_bundle), force=True) @@ -156,7 +160,9 @@ async def test_initialize_bundle_function_force_flag(bundle_storage_dir): assert len(result2) > 0, "Second initialization with force should succeed" response2_text = result2[0].text - assert "Bundle initialized" in response2_text, "Second initialization should report success" + assert ( + "Bundle initialized" in response2_text + ), "Second initialization should report success" @pytest.mark.asyncio @@ -182,7 +188,9 @@ async def test_initialize_bundle_validation_nonexistent_file(bundle_storage_dir) # Verify the error message indicates the file wasn't found error_msg = str(exc_info.value) - assert "Bundle source not found" in error_msg, "Should indicate bundle source not found" + assert ( + "Bundle source not found" in error_msg + ), "Should indicate bundle source not found" @pytest.mark.asyncio @@ -218,7 +226,9 @@ async def test_list_files_function_with_bundle(bundle_storage_dir): # Should contain file listing information assert "```json" in response_text, "Response should contain JSON data" - assert "Listed files in" in response_text, "Response should indicate listing operation" + assert ( + "Listed files in" in response_text + ), "Response should indicate listing operation" @pytest.mark.asyncio @@ -240,9 +250,9 @@ async def test_pydantic_validation_invalid_parameters(bundle_storage_dir): # Verify the error indicates path validation failure error_msg = str(exc_info.value) - assert "Path cannot contain directory traversal" in error_msg, ( - "Should indicate path validation error" - ) + assert ( + "Path cannot contain directory traversal" in error_msg + ), "Should indicate path validation error" @pytest.mark.asyncio @@ -310,7 +320,9 @@ async def test_read_file_function_execution(bundle_storage_dir): assert len(init_result) > 0, "Bundle initialization should succeed" # Try to read a common file via direct function call - read_args = ReadFileArgs(path="cluster-info/version.json", start_line=0, num_lines=10) + read_args = ReadFileArgs( + path="cluster-info/version.json", start_line=0, num_lines=10 + ) try: read_result = await read_file(read_args) diff --git a/tests/integration/test_url_fetch_auth.py b/tests/integration/test_url_fetch_auth.py index 20ccb2a..1d12bc5 100644 --- a/tests/integration/test_url_fetch_auth.py +++ b/tests/integration/test_url_fetch_auth.py @@ -53,9 +53,9 @@ def test_replicated_vendor_url_pattern_matching(self): match = REPLICATED_VENDOR_URL_PATTERN.match(url) if should_match: assert match is not None, f"URL {url} should match pattern" - assert match.group(1) == expected_slug, ( - f"Expected slug {expected_slug}, got {match.group(1)}" - ) + assert ( + match.group(1) == expected_slug + ), f"Expected slug {expected_slug}, got {match.group(1)}" else: assert match is None, f"URL {url} should not match pattern" @@ -123,7 +123,9 @@ async def test_replicated_api_401_error(self): ) with patch("httpx.AsyncClient") as mock_client: - mock_client.return_value.__aenter__.return_value.get.return_value = mock_response + mock_client.return_value.__aenter__.return_value.get.return_value = ( + mock_response + ) with pytest.raises( BundleDownloadError, @@ -147,7 +149,9 @@ async def test_replicated_api_404_error(self): ) with patch("httpx.AsyncClient") as mock_client: - mock_client.return_value.__aenter__.return_value.get.return_value = mock_response + mock_client.return_value.__aenter__.return_value.get.return_value = ( + mock_response + ) with pytest.raises( BundleDownloadError, @@ -187,7 +191,9 @@ async def test_download_size_limit_exceeded(self): mock_response.content_length = 2000000000 # 2GB with patch("aiohttp.ClientSession") as mock_session: - mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_response + mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = ( + mock_response + ) with pytest.raises( BundleDownloadError, match="Bundle size.*exceeds maximum allowed size" @@ -210,7 +216,9 @@ async def test_direct_download_auth_error(self): ) with patch("aiohttp.ClientSession") as mock_session: - mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_response + mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = ( + mock_response + ) with pytest.raises( BundleDownloadError, match="Failed to download bundle.*HTTP 401" @@ -257,14 +265,16 @@ def test_token_priority_sbctl_over_replicated(self): ): # This mirrors the logic in bundle.py line 397-398 token = os.environ.get("SBCTL_TOKEN") or os.environ.get("REPLICATED") - assert token == "sbctl-token", "SBCTL_TOKEN should take precedence over REPLICATED" + assert ( + token == "sbctl-token" + ), "SBCTL_TOKEN should take precedence over REPLICATED" with patch.dict(os.environ, {"REPLICATED": "replicated-token"}, clear=True): # When only REPLICATED is set (clear all env vars first) token = os.environ.get("SBCTL_TOKEN") or os.environ.get("REPLICATED") - assert token == "replicated-token", ( - "REPLICATED should be used when SBCTL_TOKEN is not set" - ) + assert ( + token == "replicated-token" + ), "REPLICATED should be used when SBCTL_TOKEN is not set" with patch.dict(os.environ, {}, clear=True): # When neither is set diff --git a/tests/test_all.py b/tests/test_all.py index 930a96f..038825d 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -18,4 +18,6 @@ def test_documentation(): """This is a placeholder test to explain test organization.""" - assert True, "This test always passes. The real tests are in the respective directories." + assert ( + True + ), "This test always passes. The real tests are in the respective directories." diff --git a/tests/test_utils/bundle_helpers.py b/tests/test_utils/bundle_helpers.py index 41069f8..a538ddd 100644 --- a/tests/test_utils/bundle_helpers.py +++ b/tests/test_utils/bundle_helpers.py @@ -72,11 +72,15 @@ def create_test_bundle_structure(base_dir: Path) -> Dict[str, Path]: # Create sample host files os_info = host_info / "os-info.txt" - os_info.write_text("Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n") + os_info.write_text( + "Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n" + ) structure["os_info"] = os_info memory_info = host_info / "memory.txt" - memory_info.write_text("MemTotal: 8388608 kB\nMemFree: 4194304 kB\nMemAvailable: 6291456 kB\n") + memory_info.write_text( + "MemTotal: 8388608 kB\nMemFree: 4194304 kB\nMemAvailable: 6291456 kB\n" + ) structure["memory_info"] = memory_info # Create logs directory @@ -140,7 +144,9 @@ def create_host_only_bundle_structure(base_dir: Path) -> Dict[str, Path]: # Create comprehensive host files os_info = host_info / "os-info.txt" - os_info.write_text("Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n") + os_info.write_text( + "Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n" + ) structure["os_info"] = os_info processes = host_info / "processes.txt" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 0e03404..8351007 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -56,8 +56,12 @@ def assert_api_response_valid( """ assert isinstance(response, list), "Response should be a list" assert len(response) > 0, "Response should not be empty" - assert hasattr(response[0], "type"), "Response item should have 'type' attribute" - assert response[0].type == expected_type, f"Response type should be '{expected_type}'" + assert hasattr( + response[0], "type" + ), "Response item should have 'type' attribute" + assert ( + response[0].type == expected_type + ), f"Response type should be '{expected_type}'" if contains and hasattr(response[0], "text"): for text in contains: @@ -78,9 +82,9 @@ def assert_object_matches_attrs(obj: Any, expected_attrs: Dict[str, Any]) -> Non for attr, expected in expected_attrs.items(): assert hasattr(obj, attr), f"Object should have attribute '{attr}'" actual = getattr(obj, attr) - assert actual == expected, ( - f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" - ) + assert ( + actual == expected + ), f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" @staticmethod async def assert_asyncio_timeout(coro, timeout: float = 0.1) -> None: @@ -279,7 +283,9 @@ def error_setup(tmp_path): # Create a mock process that fails error_process = AsyncMock() error_process.returncode = 1 - error_process.communicate = AsyncMock(return_value=(b"", b"Command failed with an error")) + error_process.communicate = AsyncMock( + return_value=(b"", b"Command failed with an error") + ) # Create a mock asyncio client session with errors error_session = AsyncMock() diff --git a/tests/unit/test_bundle.py b/tests/unit/test_bundle.py index e480803..e720763 100644 --- a/tests/unit/test_bundle.py +++ b/tests/unit/test_bundle.py @@ -82,7 +82,9 @@ async def test_bundle_manager_initialize_bundle_url(): manager._wait_for_initialization = AsyncMock() # Test initializing from a URL - result = await manager.initialize_bundle("https://example.com/bundle.tar.gz") + result = await manager.initialize_bundle( + "https://example.com/bundle.tar.gz" + ) # Verify the result assert isinstance(result, BundleMetadata) @@ -91,7 +93,9 @@ async def test_bundle_manager_initialize_bundle_url(): assert result.initialized is True # Verify the mocks were called - manager._download_bundle.assert_awaited_once_with("https://example.com/bundle.tar.gz") + manager._download_bundle.assert_awaited_once_with( + "https://example.com/bundle.tar.gz" + ) manager._initialize_with_sbctl.assert_awaited_once() @@ -160,7 +164,9 @@ async def test_bundle_manager_initialize_bundle_nonexistent(): REPLICATED_URL = "https://vendor.replicated.com/troubleshoot/analyze/2025-04-22@16:51" REPLICATED_SLUG = "2025-04-22@16:51" -REPLICATED_API_URL = f"https://api.replicated.com/vendor/v3/supportbundle/{REPLICATED_SLUG}" +REPLICATED_API_URL = ( + f"https://api.replicated.com/vendor/v3/supportbundle/{REPLICATED_SLUG}" +) SIGNED_URL = "https://signed.example.com/download?token=abc" @@ -228,7 +234,9 @@ async def async_iterator(): mock_aio_session.__aexit__ = AsyncMock(return_value=None) # Patch aiohttp.ClientSession to return our mock session - with patch("aiohttp.ClientSession", return_value=mock_aio_session) as mock_constructor: + with patch( + "aiohttp.ClientSession", return_value=mock_aio_session + ) as mock_constructor: # Yield the constructor, the session instance, and the response instance # This gives tests more flexibility for assertions yield mock_constructor, mock_aio_session, mock_aio_response @@ -240,7 +248,9 @@ async def test_bundle_manager_download_replicated_url_success_sbctl_token( ): """Test downloading from Replicated URL with SBCTL_TOKEN successfully.""" mock_httpx_constructor, mock_httpx_response = mock_httpx_client - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( + mock_aiohttp_download + ) with tempfile.TemporaryDirectory() as temp_dir: bundle_dir = Path(temp_dir) @@ -255,7 +265,9 @@ async def test_bundle_manager_download_replicated_url_success_sbctl_token( _, kwargs = mock_httpx_constructor.call_args assert isinstance(kwargs.get("timeout"), httpx.Timeout) - mock_get_call = mock_httpx_constructor.return_value.__aenter__.return_value.get + mock_get_call = ( + mock_httpx_constructor.return_value.__aenter__.return_value.get + ) mock_get_call.assert_awaited_once_with( REPLICATED_API_URL, headers={ @@ -273,7 +285,9 @@ async def test_bundle_manager_download_replicated_url_success_sbctl_token( assert download_path.exists() # Assert new filename format # Replace both '@' and ':' for the assertion to match sanitization - safe_slug_for_assertion = REPLICATED_SLUG.replace("@", "_").replace(":", "_") + safe_slug_for_assertion = REPLICATED_SLUG.replace("@", "_").replace( + ":", "_" + ) expected_filename_part = f"replicated_bundle_{safe_slug_for_assertion}" assert download_path.name.startswith(expected_filename_part) assert download_path.read_bytes() == b"chunk1chunk2" @@ -285,18 +299,24 @@ async def test_bundle_manager_download_replicated_url_success_replicated_token( ): """Test downloading from Replicated URL with REPLICATED_TOKEN successfully.""" mock_httpx_constructor, mock_httpx_response = mock_httpx_client - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( + mock_aiohttp_download + ) with tempfile.TemporaryDirectory() as temp_dir: bundle_dir = Path(temp_dir) manager = BundleManager(bundle_dir) # Only REPLICATED is set - with patch.dict(os.environ, {"REPLICATED": "replicated_token_value"}, clear=True): + with patch.dict( + os.environ, {"REPLICATED": "replicated_token_value"}, clear=True + ): await manager._download_bundle(REPLICATED_URL) # Verify httpx call used REPLICATED token - mock_get_call = mock_httpx_constructor.return_value.__aenter__.return_value.get + mock_get_call = ( + mock_httpx_constructor.return_value.__aenter__.return_value.get + ) mock_get_call.assert_awaited_once_with( REPLICATED_API_URL, headers={ @@ -315,7 +335,9 @@ async def test_bundle_manager_download_replicated_url_token_precedence( """Test SBCTL_TOKEN takes precedence over REPLICATED_TOKEN.""" mock_httpx_constructor, _ = mock_httpx_client # Unpack all three values from the fixture - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( + mock_aiohttp_download + ) with tempfile.TemporaryDirectory() as temp_dir: bundle_dir = Path(temp_dir) @@ -335,7 +357,9 @@ async def test_bundle_manager_download_replicated_url_token_precedence( await manager._download_bundle(REPLICATED_URL) # Verify httpx call used SBCTL_TOKEN - mock_get_call = mock_httpx_constructor.return_value.__aenter__.return_value.get + mock_get_call = ( + mock_httpx_constructor.return_value.__aenter__.return_value.get + ) mock_get_call.assert_awaited_once_with( REPLICATED_API_URL, headers={ @@ -358,7 +382,9 @@ async def test_bundle_manager_download_replicated_url_missing_token(): await manager._download_bundle(REPLICATED_URL) # === START MODIFICATION === # Update assertion to match the exact error message and correct ENV name - expected_error_part = "SBCTL_TOKEN or REPLICATED environment variable not set" + expected_error_part = ( + "SBCTL_TOKEN or REPLICATED environment variable not set" + ) assert expected_error_part in str(excinfo.value) assert "Cannot download from Replicated Vendor Portal" in str(excinfo.value) # === END MODIFICATION === @@ -373,7 +399,9 @@ async def test_bundle_manager_download_replicated_url_api_401(mock_httpx_client) mock_response.status_code = 401 mock_response.text = "Unauthorized" # Ensure json() raises an error if called on non-200 status - mock_response.json.side_effect = json.JSONDecodeError("Mock JSON decode error", "", 0) + mock_response.json.side_effect = json.JSONDecodeError( + "Mock JSON decode error", "", 0 + ) # === END MODIFICATION === with tempfile.TemporaryDirectory() as temp_dir: @@ -387,7 +415,9 @@ async def test_bundle_manager_download_replicated_url_api_401(mock_httpx_client) await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === # The error should propagate from _get_replicated_signed_url - assert "Failed to authenticate with Replicated API (status 401)" in str(excinfo.value) + assert "Failed to authenticate with Replicated API (status 401)" in str( + excinfo.value + ) @pytest.mark.asyncio @@ -397,7 +427,9 @@ async def test_bundle_manager_download_replicated_url_api_404(mock_httpx_client) # === START MODIFICATION === mock_response.status_code = 404 mock_response.text = "Not Found" - mock_response.json.side_effect = json.JSONDecodeError("Mock JSON decode error", "", 0) + mock_response.json.side_effect = json.JSONDecodeError( + "Mock JSON decode error", "", 0 + ) # === END MODIFICATION === with tempfile.TemporaryDirectory() as temp_dir: @@ -410,7 +442,9 @@ async def test_bundle_manager_download_replicated_url_api_404(mock_httpx_client) # Call _download_bundle instead of _get_replicated_signed_url await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === - assert "Support bundle not found on Replicated Vendor Portal" in str(excinfo.value) + assert "Support bundle not found on Replicated Vendor Portal" in str( + excinfo.value + ) assert f"slug: {REPLICATED_SLUG}" in str(excinfo.value) @@ -423,7 +457,9 @@ async def test_bundle_manager_download_replicated_url_api_other_error( # === START MODIFICATION === mock_response.status_code = 500 mock_response.text = "Internal Server Error" - mock_response.json.side_effect = json.JSONDecodeError("Mock JSON decode error", "", 0) + mock_response.json.side_effect = json.JSONDecodeError( + "Mock JSON decode error", "", 0 + ) # === END MODIFICATION === with tempfile.TemporaryDirectory() as temp_dir: @@ -436,8 +472,12 @@ async def test_bundle_manager_download_replicated_url_api_other_error( # Call _download_bundle instead of _get_replicated_signed_url await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === - assert "Failed to get signed URL from Replicated API (status 500)" in str(excinfo.value) - assert "Internal Server Error" in str(excinfo.value) # Check response text included + assert "Failed to get signed URL from Replicated API (status 500)" in str( + excinfo.value + ) + assert "Internal Server Error" in str( + excinfo.value + ) # Check response text included @pytest.mark.asyncio @@ -465,7 +505,9 @@ async def test_bundle_manager_download_replicated_url_missing_signed_uri( # Call _download_bundle instead of _get_replicated_signed_url await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === - assert "Could not find 'signedUri' in Replicated API response" in str(excinfo.value) + assert "Could not find 'signedUri' in Replicated API response" in str( + excinfo.value + ) @pytest.mark.asyncio @@ -487,14 +529,18 @@ async def test_bundle_manager_download_replicated_url_network_error(): # Assert that the correct error (raised by the except httpx.RequestError block) is caught assert "Network error requesting signed URL" in str(excinfo.value) - assert "Network timeout" in str(excinfo.value) # Check original error is included + assert "Network timeout" in str( + excinfo.value + ) # Check original error is included # === END MODIFICATION === @pytest.mark.asyncio async def test_bundle_manager_download_non_replicated_url(mock_aiohttp_download): """Test that non-Replicated URLs are downloaded directly without API calls.""" - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( + mock_aiohttp_download + ) non_replicated_url = "https://normal.example.com/bundle.tar.gz" with tempfile.TemporaryDirectory() as temp_dir: @@ -529,7 +575,9 @@ async def test_bundle_manager_download_bundle( ): # Use fixture as argument """Test that the bundle manager can download a non-Replicated bundle.""" # Unpack the fixture results - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( + mock_aiohttp_download + ) non_replicated_url = "https://example.com/bundle.tar.gz" with tempfile.TemporaryDirectory() as temp_dir: @@ -872,7 +920,9 @@ async def test_bundle_manager_server_shutdown_cleanup(): manager._cleanup_active_bundle = AsyncMock() # Mock subprocess.run to avoid actual process operations - with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="", stderr="")): + with patch( + "subprocess.run", return_value=MagicMock(returncode=0, stdout="", stderr="") + ): # Call cleanup await manager.cleanup() @@ -891,7 +941,9 @@ async def test_bundle_manager_server_shutdown_cleanup(): mock_pkill_result.returncode = 0 # Mock subprocess to return our mock objects - with patch("subprocess.run", side_effect=[mock_ps_result, mock_pkill_result]): + with patch( + "subprocess.run", side_effect=[mock_ps_result, mock_pkill_result] + ): # Test with orphaned processes await manager.cleanup() @@ -954,8 +1006,12 @@ async def test_bundle_manager_host_only_bundle_detection(): assert args[1] == "serve" assert args[2] == "--support-bundle-location" # Verify bundle path is in the arguments - bundle_arg_found = any(str(local_bundle_path) in str(arg) for arg in args) - assert bundle_arg_found, f"Bundle path not found in subprocess args: {args}" + bundle_arg_found = any( + str(local_bundle_path) in str(arg) for arg in args + ) + assert ( + bundle_arg_found + ), f"Bundle path not found in subprocess args: {args}" # Note: We test regular bundles in the existing tests that already work properly diff --git a/tests/unit/test_bundle_cleanup_dependencies.py b/tests/unit/test_bundle_cleanup_dependencies.py index 22997fb..a159fb8 100644 --- a/tests/unit/test_bundle_cleanup_dependencies.py +++ b/tests/unit/test_bundle_cleanup_dependencies.py @@ -70,7 +70,9 @@ async def test_bundle_cleanup_functional_dependency_validation(): print("✅ No missing dependencies detected in cleanup process") # Verify cleanup actually did something - assert bundle_manager.active_bundle is None, "Bundle should be cleared after cleanup" + assert ( + bundle_manager.active_bundle is None + ), "Bundle should be cleared after cleanup" except FileNotFoundError as e: # This is what we expect to see if dependencies are missing in container @@ -216,7 +218,9 @@ def test_container_environment_simulation(): print( f"✅ Confirmed {len(unavailable_commands)} commands unavailable: {unavailable_commands}" ) - print("✅ This environment would have broken the original ps/pkill subprocess calls") + print( + "✅ This environment would have broken the original ps/pkill subprocess calls" + ) print("✅ The psutil fix ensures cleanup works even without external commands") finally: diff --git a/tests/unit/test_components.py b/tests/unit/test_components.py index edd316a..299d34e 100755 --- a/tests/unit/test_components.py +++ b/tests/unit/test_components.py @@ -65,7 +65,9 @@ async def test_bundle_initialization(mock_command_environment, fixtures_dir): try: # Initialize the bundle logger.info("Initializing bundle...") - metadata = await bundle_manager.initialize_bundle(str(test_bundle_copy), force=True) + metadata = await bundle_manager.initialize_bundle( + str(test_bundle_copy), force=True + ) # Verify expected behavior assert metadata.initialized, "Bundle should be marked as initialized" @@ -73,9 +75,9 @@ async def test_bundle_initialization(mock_command_environment, fixtures_dir): # Verify diagnostic information diagnostics = await bundle_manager.get_diagnostic_info() - assert diagnostics["bundle_initialized"], ( - "Bundle should be marked as initialized in diagnostics" - ) + assert diagnostics[ + "bundle_initialized" + ], "Bundle should be marked as initialized in diagnostics" # Clean up the bundle manager await bundle_manager.cleanup() @@ -125,9 +127,13 @@ async def test_kubectl_execution(mock_command_environment, fixtures_dir): try: # Initialize the bundle first - metadata = await bundle_manager.initialize_bundle(str(test_bundle_copy), force=True) + metadata = await bundle_manager.initialize_bundle( + str(test_bundle_copy), force=True + ) assert metadata.initialized, "Bundle should be initialized successfully" - assert metadata.kubeconfig_path.exists(), "Kubeconfig should exist after initialization" + assert ( + metadata.kubeconfig_path.exists() + ), "Kubeconfig should exist after initialization" # Set KUBECONFIG environment variable for kubectl os.environ["KUBECONFIG"] = str(metadata.kubeconfig_path) @@ -144,10 +150,14 @@ async def test_kubectl_execution(mock_command_environment, fixtures_dir): assert proc.returncode == 0, "kubectl should be available in PATH" # Now run a command with the executor - result = await asyncio.wait_for(kubectl_executor.execute("get nodes"), timeout=10.0) + result = await asyncio.wait_for( + kubectl_executor.execute("get nodes"), timeout=10.0 + ) # Verify the command result behavior - assert result.exit_code == 0, f"Command should succeed, got error: {result.stderr}" + assert ( + result.exit_code == 0 + ), f"Command should succeed, got error: {result.stderr}" assert result.stdout, "Command should produce output" assert isinstance(result.duration_ms, int), "Duration should be measured" assert result.duration_ms > 0, "Duration should be positive" @@ -209,7 +219,9 @@ async def test_file_explorer_behavior(test_file_setup): assert list_result.total_files >= 1, "Should find at least one file" # Log what's found for debugging - logger.info(f"Found {list_result.total_dirs} directories and {list_result.total_files} files") + logger.info( + f"Found {list_result.total_dirs} directories and {list_result.total_files} files" + ) # Test 2: Test reading a specific file from the test directory # We know from fixture setup that dir1/file1.txt exists @@ -228,14 +240,16 @@ async def test_file_explorer_behavior(test_file_setup): assert grep_result.files_searched > 0, "Should search multiple files" # At least one match should be from our files - assert any("file" in match.line for match in grep_result.matches), ( - "Should find 'file' string in matches" - ) + assert any( + "file" in match.line for match in grep_result.matches + ), "Should find 'file' string in matches" # Test 4: Case sensitivity behavior # Our test_file_setup fixture creates a file with UPPERCASE text for these tests case_sensitive = await file_explorer.grep_files("UPPERCASE", "", True, None, True) - case_insensitive = await file_explorer.grep_files("uppercase", "", True, None, False) + case_insensitive = await file_explorer.grep_files( + "uppercase", "", True, None, False + ) # Verify case sensitivity behavior assert case_sensitive.total_matches > 0, "Should find case-sensitive matches" diff --git a/tests/unit/test_curl_dependency_reproduction.py b/tests/unit/test_curl_dependency_reproduction.py index 22ad344..565e173 100644 --- a/tests/unit/test_curl_dependency_reproduction.py +++ b/tests/unit/test_curl_dependency_reproduction.py @@ -195,9 +195,9 @@ async def test_curl_dependency_cascading_failure_to_kubectl( ): # Test that API server check fails due to curl dependency api_available = await bundle_manager.check_api_server_available() - assert api_available is False, ( - "API server should be unavailable when curl is missing" - ) + assert ( + api_available is False + ), "API server should be unavailable when curl is missing" # This demonstrates the cascading failure: # 1. curl is missing -> check_api_server_available() returns False @@ -213,9 +213,9 @@ async def test_curl_dependency_cascading_failure_to_kubectl( # Verify this by checking that the diagnostic info shows the problem diagnostics = await bundle_manager.get_diagnostic_info() - assert diagnostics["api_server_available"] is False, ( - "Diagnostics should show API server as unavailable due to curl dependency" - ) + assert ( + diagnostics["api_server_available"] is False + ), "Diagnostics should show API server as unavailable due to curl dependency" @pytest.mark.asyncio @@ -276,9 +276,9 @@ async def mock_subprocess_immediate_failure(*args, **kwargs): # The curl dependency failure should occur immediately, before any timeout result = await bundle_manager.check_api_server_available() - assert result is False, ( - "Should fail immediately due to missing curl, before any timeout" - ) + assert ( + result is False + ), "Should fail immediately due to missing curl, before any timeout" @pytest.mark.asyncio @@ -345,7 +345,9 @@ async def mock_subprocess_env_specific(*args, **kwargs): ): result = await bundle_manager.check_api_server_available() - assert result is False, f"Should fail in {env['name']} environment without curl" + assert ( + result is False + ), f"Should fail in {env['name']} environment without curl" @pytest.mark.asyncio diff --git a/tests/unit/test_files.py b/tests/unit/test_files.py index 96968c6..eac140a 100644 --- a/tests/unit/test_files.py +++ b/tests/unit/test_files.py @@ -122,7 +122,9 @@ def test_grep_files_args_validation(): assert args_defaults.max_files == 10 # Default value # Test new parameters with custom values - args_custom = GrepFilesArgs(pattern="test", path="dir1", max_results_per_file=3, max_files=5) + args_custom = GrepFilesArgs( + pattern="test", path="dir1", max_results_per_file=3, max_files=5 + ) assert args_custom.max_results_per_file == 3 assert args_custom.max_files == 5 @@ -178,9 +180,13 @@ async def test_file_explorer_list_files(): result = await explorer.list_files("cluster-resources", True) # Verify behavior expectations for recursive listing - assert result.path == "cluster-resources", "Path should match requested directory" + assert ( + result.path == "cluster-resources" + ), "Path should match requested directory" assert result.recursive is True, "Recursive flag should be preserved" - assert result.total_files >= 1, "Should find at least 1 file in cluster-resources" + assert ( + result.total_files >= 1 + ), "Should find at least 1 file in cluster-resources" # Test 3: Verify result structure is correct (behavior contracts) for entry in result.entries: @@ -267,27 +273,37 @@ async def test_file_explorer_read_file(): result = await explorer.read_file("cluster-resources/pods/kube-system.json") # Verify behavior expectations - assert isinstance(result, FileContentResult), "Result should be a FileContentResult" - assert result.path == "cluster-resources/pods/kube-system.json", ( - "Path should be preserved in result" - ) - assert "test-pod" in result.content, "Content should match expected JSON content" + assert isinstance( + result, FileContentResult + ), "Result should be a FileContentResult" + assert ( + result.path == "cluster-resources/pods/kube-system.json" + ), "Path should be preserved in result" + assert ( + "test-pod" in result.content + ), "Content should match expected JSON content" assert result.binary is False, "JSON file should not be marked as binary" assert result.total_lines > 0, "Line count should be available" # Test 2: Reading a line range from the same file - result = await explorer.read_file("cluster-resources/pods/kube-system.json", 1, 3) + result = await explorer.read_file( + "cluster-resources/pods/kube-system.json", 1, 3 + ) # Verify behavior expectations for line ranges assert result.start_line == 1, "Start line should match requested value" assert result.end_line >= 1, "End line should be at least start line" - assert len(result.content.split("\n")) <= 4, "Should have limited lines based on range" + assert ( + len(result.content.split("\n")) <= 4 + ), "Should have limited lines based on range" # Test 3: Reading binary file (from the with_binaries structure) result = await explorer.read_file("binaries/fake_binary") # Verify behavior expectations for binary files - assert result.path == "binaries/fake_binary", "Path should be preserved in result" + assert ( + result.path == "binaries/fake_binary" + ), "Path should be preserved in result" assert result.binary is True, "Binary file should be marked as binary" @@ -353,7 +369,9 @@ async def test_file_explorer_grep_files(): test_dir.mkdir(exist_ok=True) # Create files with specific content for pattern matching - (test_dir / "case_test.txt").write_text("This contains UPPERCASE and lowercase text\n") + (test_dir / "case_test.txt").write_text( + "This contains UPPERCASE and lowercase text\n" + ) (test_dir / "pattern_test.txt").write_text( "This is a test file with patterns\nAnother line with test word\n" ) @@ -409,8 +427,12 @@ async def test_file_explorer_grep_files(): # Verify behavior expectations for case sensitivity assert case_sensitive.total_matches >= 1, "Should find exact case matches" - assert case_insensitive.total_matches >= 1, "Should find case-insensitive matches" - assert case_insensitive.case_sensitive is False, "Should preserve case sensitivity flag" + assert ( + case_insensitive.total_matches >= 1 + ), "Should find case-insensitive matches" + assert ( + case_insensitive.case_sensitive is False + ), "Should preserve case sensitivity flag" @pytest.mark.asyncio @@ -470,7 +492,9 @@ async def test_file_explorer_grep_files_with_kubeconfig(): # There should be matches in our reference file ref_file_matches = [m for m in result.matches if "reference.txt" in m.path] - assert len(ref_file_matches) >= 3, "Should find multiple matches in reference.txt" + assert ( + len(ref_file_matches) >= 3 + ), "Should find multiple matches in reference.txt" @pytest.mark.asyncio @@ -539,11 +563,15 @@ def test_file_explorer_is_binary(): # Test 1: Text file should not be marked as binary (JSON file from real structure) text_file_path = bundle_structure["kube_system_pods"] - assert not explorer._is_binary(text_file_path), "JSON file should not be detected as binary" + assert not explorer._is_binary( + text_file_path + ), "JSON file should not be detected as binary" # Test 2: Binary file should be marked as binary (real binary file) binary_file_path = bundle_structure["fake_binary"] - assert explorer._is_binary(binary_file_path), "Binary file should be detected as binary" + assert explorer._is_binary( + binary_file_path + ), "Binary file should be detected as binary" def test_file_explorer_normalize_path(): @@ -576,21 +604,21 @@ def test_file_explorer_normalize_path(): # Test 1: Normalizing a relative path normalized = explorer._normalize_path("cluster-resources") - assert normalized == bundle_path / "cluster-resources", ( - "Relative path should be resolved to absolute path" - ) + assert ( + normalized == bundle_path / "cluster-resources" + ), "Relative path should be resolved to absolute path" # Test 2: Normalizing a path with leading slashes normalized = explorer._normalize_path("/cluster-resources") - assert normalized == bundle_path / "cluster-resources", ( - "Leading slashes should be handled properly" - ) + assert ( + normalized == bundle_path / "cluster-resources" + ), "Leading slashes should be handled properly" # Test 3: Normalizing a nested path normalized = explorer._normalize_path("cluster-resources/pods") - assert normalized == bundle_path / "cluster-resources" / "pods", ( - "Nested paths should be resolved correctly" - ) + assert ( + normalized == bundle_path / "cluster-resources" / "pods" + ), "Nested paths should be resolved correctly" # Test 4: Security check - block directory traversal attempts with pytest.raises(InvalidPathError): diff --git a/tests/unit/test_files_parametrized.py b/tests/unit/test_files_parametrized.py index ebeb305..5e2ed8e 100644 --- a/tests/unit/test_files_parametrized.py +++ b/tests/unit/test_files_parametrized.py @@ -121,7 +121,9 @@ def test_list_files_args_validation_parametrized(path, recursive, expected_valid "invalid-negative-end", ], ) -def test_read_file_args_validation_parametrized(path, start_line, end_line, expected_valid): +def test_read_file_args_validation_parametrized( + path, start_line, end_line, expected_valid +): """ Test ReadFileArgs validation with parameterized test cases. diff --git a/tests/unit/test_fixes_integration.py b/tests/unit/test_fixes_integration.py index 94f4143..bb6feaf 100644 --- a/tests/unit/test_fixes_integration.py +++ b/tests/unit/test_fixes_integration.py @@ -23,7 +23,9 @@ async def test_transport_cleanup_fix_integration(): works correctly with subprocess_utils. """ if sys.version_info < (3, 13): - pytest.skip("This test is specifically for Python 3.13+ transport fix verification") + pytest.skip( + "This test is specifically for Python 3.13+ transport fix verification" + ) from mcp_server_troubleshoot.subprocess_utils import subprocess_exec_with_cleanup @@ -32,7 +34,10 @@ async def test_transport_cleanup_fix_integration(): def warning_capture(message, category, filename, lineno, file=None, line=None): msg_str = str(message) - if any(keyword in msg_str.lower() for keyword in ["transport", "_closing", "unclosed"]): + if any( + keyword in msg_str.lower() + for keyword in ["transport", "_closing", "unclosed"] + ): transport_issues.append(f"{category.__name__}: {msg_str}") original_showwarning = warnings.showwarning @@ -58,7 +63,9 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): f"The Python 3.13 transport cleanup fix may not be working correctly." ) - print("✅ Python 3.13 transport cleanup fix verified: No transport issues detected") + print( + "✅ Python 3.13 transport cleanup fix verified: No transport issues detected" + ) finally: warnings.showwarning = original_showwarning @@ -83,12 +90,18 @@ async def test_socket_port_checking_fix_integration(): try: # This should work without any netstat dependency result = bundle_manager._check_port_listening_python(port) - assert isinstance(result, bool), f"Port check should return boolean for port {port}" - print(f"✅ Port {port} check successful: {result} (socket-based, no netstat required)") + assert isinstance( + result, bool + ), f"Port check should return boolean for port {port}" + print( + f"✅ Port {port} check successful: {result} (socket-based, no netstat required)" + ) except Exception as e: pytest.fail(f"Socket-based port checking failed for port {port}: {e}") - print("✅ Socket-based port checking fix verified: All ports checked without netstat") + print( + "✅ Socket-based port checking fix verified: All ports checked without netstat" + ) @pytest.mark.asyncio @@ -113,11 +126,15 @@ async def test_netstat_replaced_in_diagnostic_info(): diagnostic_info = await bundle_manager.get_diagnostic_info() # Verify we got diagnostic information - assert isinstance(diagnostic_info, dict), "Diagnostic info should be a dictionary" + assert isinstance( + diagnostic_info, dict + ), "Diagnostic info should be a dictionary" # Look for evidence that socket-based checking was used port_checked_keys = [ - key for key in diagnostic_info.keys() if "port_" in key and "_checked" in key + key + for key in diagnostic_info.keys() + if "port_" in key and "_checked" in key ] # Note: Port checking only happens when sbctl is available @@ -130,13 +147,19 @@ async def test_netstat_replaced_in_diagnostic_info(): assert len(port_checked_keys) > 0, "Should have checked at least one port" # Verify no netstat-related errors - netstat_error_keys = [key for key in diagnostic_info.keys() if "netstat" in key.lower()] + netstat_error_keys = [ + key for key in diagnostic_info.keys() if "netstat" in key.lower() + ] if netstat_error_keys: # If there are netstat-related keys, they should be from old code, not our new code - print(f"Found netstat-related keys (may be from old code): {netstat_error_keys}") + print( + f"Found netstat-related keys (may be from old code): {netstat_error_keys}" + ) # Look for socket-based port checking evidence - socket_evidence = [key for key in diagnostic_info.keys() if "socket" in key.lower()] + socket_evidence = [ + key for key in diagnostic_info.keys() if "socket" in key.lower() + ] if socket_evidence: print(f"✅ Found evidence of socket-based checking: {socket_evidence}") @@ -164,7 +187,10 @@ async def test_both_fixes_work_together(): def warning_capture(message, category, filename, lineno, file=None, line=None): msg_str = str(message) - if any(keyword in msg_str.lower() for keyword in ["transport", "_closing", "unclosed"]): + if any( + keyword in msg_str.lower() + for keyword in ["transport", "_closing", "unclosed"] + ): transport_issues.append(f"{category.__name__}: {msg_str}") original_showwarning = warnings.showwarning @@ -189,7 +215,9 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): test_ports = [8080, 9090, 3000] for port in test_ports: port_status = bundle_manager._check_port_listening_python(port) - assert isinstance(port_status, bool), f"Port {port} check should return boolean" + assert isinstance( + port_status, bool + ), f"Port {port} check should return boolean" # Test full diagnostic info (integration of both fixes) diagnostic_info = await bundle_manager.get_diagnostic_info() @@ -201,10 +229,14 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.1) # Verify both fixes work - assert len(transport_issues) == 0, f"No transport issues should occur: {transport_issues}" + assert ( + len(transport_issues) == 0 + ), f"No transport issues should occur: {transport_issues}" port_checked_keys = [ - key for key in diagnostic_info.keys() if "port_" in key and "_checked" in key + key + for key in diagnostic_info.keys() + if "port_" in key and "_checked" in key ] # Note: Port checking only happens when sbctl is available @@ -214,7 +246,9 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): print("✅ Socket-based port checking is ready when sbctl is present") else: print(f"✅ Port checking worked: {port_checked_keys}") - assert len(port_checked_keys) > 0, "Port checking should work without netstat" + assert ( + len(port_checked_keys) > 0 + ), "Port checking should work without netstat" print("✅ Both fixes work correctly together:") print(" - Python 3.13 transport cleanup: No transport issues") @@ -237,9 +271,15 @@ def test_fixes_are_properly_implemented(): source = inspect.getsource(subprocess_utils) # Verify Python 3.13 specific code is present - assert "sys.version_info >= (3, 13)" in source, "Should have Python 3.13 version check" - assert "_safe_transport_cleanup" in source, "Should have safe transport cleanup function" - assert "_safe_transport_wait_close" in source, "Should have safe transport wait function" + assert ( + "sys.version_info >= (3, 13)" in source + ), "Should have Python 3.13 version check" + assert ( + "_safe_transport_cleanup" in source + ), "Should have safe transport cleanup function" + assert ( + "_safe_transport_wait_close" in source + ), "Should have safe transport wait function" # Check bundle.py has socket-based port checking from mcp_server_troubleshoot import bundle @@ -248,12 +288,12 @@ def test_fixes_are_properly_implemented(): # Verify socket import and usage assert "import socket" in bundle_source, "Should import socket module" - assert "_check_port_listening_python" in bundle_source, ( - "Should have Python port checking method" - ) - assert "socket.socket(socket.AF_INET, socket.SOCK_STREAM)" in bundle_source, ( - "Should use socket for port checking" - ) + assert ( + "_check_port_listening_python" in bundle_source + ), "Should have Python port checking method" + assert ( + "socket.socket(socket.AF_INET, socket.SOCK_STREAM)" in bundle_source + ), "Should use socket for port checking" # Verify netstat is no longer used in the port checking code # (It might still be mentioned in comments or documentation) @@ -262,9 +302,9 @@ def test_fixes_are_properly_implemented(): if "netstat" in line.lower() and "subprocess_exec_with_cleanup(" in line: netstat_usage_lines.append(f"Line {line_num}: {line.strip()}") - assert len(netstat_usage_lines) == 0, ( - f"Found active netstat usage that should be replaced: {netstat_usage_lines}" - ) + assert ( + len(netstat_usage_lines) == 0 + ), f"Found active netstat usage that should be replaced: {netstat_usage_lines}" print("✅ Both fixes are properly implemented in the codebase:") print(" - subprocess_utils: Python 3.13 transport handling") diff --git a/tests/unit/test_grep_fix.py b/tests/unit/test_grep_fix.py index ccdba76..8a89adf 100644 --- a/tests/unit/test_grep_fix.py +++ b/tests/unit/test_grep_fix.py @@ -30,7 +30,9 @@ async def test_grep_files_with_kubeconfig(tmp_path: Path): # Create a kubeconfig file kubeconfig_path = bundle_dir / "kubeconfig" - kubeconfig_path.write_text("apiVersion: v1\nkind: Config\nclusters:\n- name: test-cluster\n") + kubeconfig_path.write_text( + "apiVersion: v1\nkind: Config\nclusters:\n- name: test-cluster\n" + ) print(f"Created kubeconfig file at: {kubeconfig_path}") # Create a directory with a few test files @@ -39,7 +41,9 @@ async def test_grep_files_with_kubeconfig(tmp_path: Path): # Create a file with 'kubeconfig' in its contents ref_file = test_dir / "config-reference.txt" - ref_file.write_text("This file refers to a kubeconfig file.\nThe kubeconfig path is important.") + ref_file.write_text( + "This file refers to a kubeconfig file.\nThe kubeconfig path is important." + ) print(f"Created reference file at: {ref_file}") # Create a nested kubeconfig file @@ -65,7 +69,9 @@ async def test_grep_files_with_kubeconfig(tmp_path: Path): explorer = FileExplorer(bundle_manager) # Test searching for "kubeconfig" (case insensitive) - result = await explorer.grep_files("kubeconfig", "", recursive=True, case_sensitive=False) + result = await explorer.grep_files( + "kubeconfig", "", recursive=True, case_sensitive=False + ) # Print the results print("\nSearch results for 'kubeconfig':") diff --git a/tests/unit/test_kubectl.py b/tests/unit/test_kubectl.py index c1438f0..3ec1138 100644 --- a/tests/unit/test_kubectl.py +++ b/tests/unit/test_kubectl.py @@ -144,7 +144,9 @@ async def test_kubectl_executor_execute_success(): # Verify the result assert result == mock_result - executor._run_kubectl_command.assert_awaited_once_with("get pods", bundle, 30, False) + executor._run_kubectl_command.assert_awaited_once_with( + "get pods", bundle, 30, False + ) @pytest.mark.asyncio @@ -170,7 +172,9 @@ async def test_kubectl_executor_run_kubectl_command(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + with patch( + "asyncio.create_subprocess_exec", return_value=mock_process + ) as mock_exec: # Execute a command result = await executor._run_kubectl_command("get pods", bundle, 30, True) @@ -221,7 +225,9 @@ async def test_kubectl_executor_run_kubectl_command_no_json(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + with patch( + "asyncio.create_subprocess_exec", return_value=mock_process + ) as mock_exec: # Execute a command result = await executor._run_kubectl_command("get pods", bundle, 30, False) @@ -263,15 +269,21 @@ async def test_kubectl_executor_run_kubectl_command_explicit_format(): # Mock subprocess mock_process = AsyncMock() mock_process.returncode = 0 - mock_process.communicate = AsyncMock(return_value=(b"name: pod1\nstatus: Running", b"")) + mock_process.communicate = AsyncMock( + return_value=(b"name: pod1\nstatus: Running", b"") + ) # Create the executor executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + with patch( + "asyncio.create_subprocess_exec", return_value=mock_process + ) as mock_exec: # Execute a command with explicit format - result = await executor._run_kubectl_command("get pods -o yaml", bundle, 30, True) + result = await executor._run_kubectl_command( + "get pods -o yaml", bundle, 30, True + ) # Verify the result assert result.command == "get pods -o yaml" @@ -309,7 +321,9 @@ async def test_kubectl_executor_run_kubectl_command_error(): # Mock subprocess mock_process = AsyncMock() mock_process.returncode = 1 - mock_process.communicate = AsyncMock(return_value=(b"", b'Error: resource "pods" not found')) + mock_process.communicate = AsyncMock( + return_value=(b"", b'Error: resource "pods" not found') + ) # Create the executor executor = KubectlExecutor(bundle_manager) @@ -365,7 +379,9 @@ async def mock_subprocess_timeout(*args, **kwargs): ): # Execute a command with a short timeout with pytest.raises(KubectlError) as excinfo: - await executor._run_kubectl_command("get pods", bundle, 0.1, True) # 0.1 second timeout + await executor._run_kubectl_command( + "get pods", bundle, 0.1, True + ) # 0.1 second timeout # Verify the error assert "kubectl command timed out" in str(excinfo.value) @@ -430,7 +446,9 @@ async def test_kubectl_default_cli_format(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + with patch( + "asyncio.create_subprocess_exec", return_value=mock_process + ) as mock_exec: # Execute with default json_output=False result = await executor._run_kubectl_command("get pods", bundle, 30, False) @@ -471,7 +489,9 @@ async def test_kubectl_explicit_json_request(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + with patch( + "asyncio.create_subprocess_exec", return_value=mock_process + ) as mock_exec: # Execute with explicit json_output=True result = await executor._run_kubectl_command("get pods", bundle, 30, True) @@ -512,9 +532,13 @@ async def test_kubectl_user_format_preserved(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + with patch( + "asyncio.create_subprocess_exec", return_value=mock_process + ) as mock_exec: # Execute with user-specified YAML format - result = await executor._run_kubectl_command("get pods -o yaml", bundle, 30, False) + result = await executor._run_kubectl_command( + "get pods -o yaml", bundle, 30, False + ) # Verify user format is preserved assert result.command == "get pods -o yaml" # No modification @@ -560,11 +584,15 @@ async def test_kubectl_executor_defaults_to_table_format(): result = await executor.execute("get pods") # Verify the result is NOT JSON - assert result.is_json is False, "Default kubectl execution should NOT return JSON format" + assert ( + result.is_json is False + ), "Default kubectl execution should NOT return JSON format" assert result == mock_result # Verify _run_kubectl_command was called with json_output=False (the new default) - executor._run_kubectl_command.assert_awaited_once_with("get pods", bundle, 30, False) + executor._run_kubectl_command.assert_awaited_once_with( + "get pods", bundle, 30, False + ) def test_compact_json_formatting(): diff --git a/tests/unit/test_kubectl_parametrized.py b/tests/unit/test_kubectl_parametrized.py index feb4bf5..6851dde 100644 --- a/tests/unit/test_kubectl_parametrized.py +++ b/tests/unit/test_kubectl_parametrized.py @@ -78,14 +78,18 @@ def test_kubectl_command_args_validation_parametrized( """ if expected_valid: # Should succeed - args = KubectlCommandArgs(command=command, timeout=timeout, json_output=json_output) + args = KubectlCommandArgs( + command=command, timeout=timeout, json_output=json_output + ) assert args.command == command assert args.timeout == timeout assert args.json_output == json_output else: # Should raise ValidationError with pytest.raises(ValidationError): - KubectlCommandArgs(command=command, timeout=timeout, json_output=json_output) + KubectlCommandArgs( + command=command, timeout=timeout, json_output=json_output + ) @pytest.mark.asyncio @@ -120,7 +124,9 @@ def test_kubectl_command_args_validation_parametrized( "version", ], ) -async def test_kubectl_command_execution_parameters(command, expected_args, add_json, test_factory): +async def test_kubectl_command_execution_parameters( + command, expected_args, add_json, test_factory +): """ Test that the kubectl executor handles different command formats correctly. @@ -151,7 +157,9 @@ async def test_kubectl_command_execution_parameters(command, expected_args, add_ expected_args.extend(["-o", "json"]) # Mock the create_subprocess_exec function - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + with patch( + "asyncio.create_subprocess_exec", return_value=mock_process + ) as mock_exec: # Execute the command result = await executor._run_kubectl_command(command, bundle, 30, True) @@ -371,7 +379,9 @@ async def test_kubectl_response_parsing(test_assertions, test_factory): assert is_json == case["expected_is_json"], f"Case {i}: JSON detection failed" # Assert the output was processed to the right type - assert isinstance(processed, case["expected_type"]), f"Case {i}: Wrong output type" + assert isinstance( + processed, case["expected_type"] + ), f"Case {i}: Wrong output type" # For JSON outputs, verify structure if case["expected_is_json"]: diff --git a/tests/unit/test_lifecycle.py b/tests/unit/test_lifecycle.py index a202059..6325a26 100644 --- a/tests/unit/test_lifecycle.py +++ b/tests/unit/test_lifecycle.py @@ -42,7 +42,9 @@ async def test_periodic_bundle_cleanup(): mock_bundle_manager = AsyncMock() # Create a task with a short interval - task = asyncio.create_task(periodic_bundle_cleanup(mock_bundle_manager, interval=0.1)) + task = asyncio.create_task( + periodic_bundle_cleanup(mock_bundle_manager, interval=0.1) + ) # Let it run for a short time await asyncio.sleep(0.3) @@ -66,7 +68,9 @@ async def test_lifecycle_context_normal_exit(): mock_server.use_stdio = True # Set environment variables for the test - with patch.dict(os.environ, {"ENABLE_PERIODIC_CLEANUP": "true", "CLEANUP_INTERVAL": "60"}): + with patch.dict( + os.environ, {"ENABLE_PERIODIC_CLEANUP": "true", "CLEANUP_INTERVAL": "60"} + ): # Enter the context manager async with app_lifespan(mock_server) as context: # Verify resources were initialized diff --git a/tests/unit/test_list_bundles.py b/tests/unit/test_list_bundles.py index 823c97e..46e344c 100644 --- a/tests/unit/test_list_bundles.py +++ b/tests/unit/test_list_bundles.py @@ -79,7 +79,9 @@ async def test_list_available_bundles_valid_bundle(temp_bundle_dir, mock_valid_b @pytest.mark.asyncio -async def test_list_available_bundles_invalid_bundle(temp_bundle_dir, mock_invalid_bundle): +async def test_list_available_bundles_invalid_bundle( + temp_bundle_dir, mock_invalid_bundle +): """Test listing bundles with an invalid bundle.""" bundle_manager = BundleManager(temp_bundle_dir) @@ -154,7 +156,9 @@ async def test_bundle_validity_checker( assert message is not None # Non-existing file - valid, message = bundle_manager._check_bundle_validity(Path("/non/existing/file.tar.gz")) + valid, message = bundle_manager._check_bundle_validity( + Path("/non/existing/file.tar.gz") + ) assert valid is False assert message is not None @@ -187,7 +191,9 @@ async def test_relative_path_initialization(temp_bundle_dir, mock_valid_bundle): # Instead of monkeypatching internal methods, we'll mock at a higher level # This focuses on the behavior (initializing a bundle) rather than implementation - with patch.object(bundle_manager, "_initialize_with_sbctl", autospec=False) as mock_init: + with patch.object( + bundle_manager, "_initialize_with_sbctl", autospec=False + ) as mock_init: # Set up the mock to create the kubeconfig file and return its path async def side_effect(bundle_path, output_dir): logger.info(f"Creating mock kubeconfig in {output_dir}") @@ -236,7 +242,9 @@ async def test_bundle_path_resolution_behavior(temp_bundle_dir, mock_valid_bundl bundle_manager = BundleManager(temp_bundle_dir) # Create patch for _initialize_with_sbctl to avoid actual initialization - with patch.object(bundle_manager, "_initialize_with_sbctl", autospec=False) as mock_init: + with patch.object( + bundle_manager, "_initialize_with_sbctl", autospec=False + ) as mock_init: # Set up the mock to return a valid kubeconfig path async def side_effect(bundle_path, output_dir): os.makedirs(output_dir, exist_ok=True) diff --git a/tests/unit/test_netstat_dependency.py b/tests/unit/test_netstat_dependency.py index 8048f40..2a5b052 100644 --- a/tests/unit/test_netstat_dependency.py +++ b/tests/unit/test_netstat_dependency.py @@ -35,11 +35,15 @@ async def test_bundle_api_check_without_netstat(): clean_path_parts = [] for path_part in original_path.split(":"): # Skip paths that typically contain netstat - if not any(common in path_part.lower() for common in ["/bin", "/sbin", "/usr"]): + if not any( + common in path_part.lower() for common in ["/bin", "/sbin", "/usr"] + ): clean_path_parts.append(path_part) # Set a very minimal PATH that won't have netstat - os.environ["PATH"] = ":".join(clean_path_parts) if clean_path_parts else "/nonexistent" + os.environ["PATH"] = ( + ":".join(clean_path_parts) if clean_path_parts else "/nonexistent" + ) # Now try to execute netstat - this should fail with FileNotFoundError try: @@ -207,7 +211,9 @@ def check_port_listening_python(port: int) -> bool: try: # Test on a port that's likely free - port_free = check_port_listening_python(0) # Port 0 is special - always available + port_free = check_port_listening_python( + 0 + ) # Port 0 is special - always available # Test on a port that might be in use port_in_use = check_port_listening_python(22) # SSH port often in use @@ -222,7 +228,9 @@ def check_port_listening_python(port: int) -> bool: # For TDD: The current (netstat) approach should fail, # and the replacement (Python socket) approach should work if netstat_failed and python_socket_works: - print("✅ TDD SUCCESS: Demonstrated the netstat dependency problem and solution!") + print( + "✅ TDD SUCCESS: Demonstrated the netstat dependency problem and solution!" + ) print(f"❌ Current netstat approach failed: {netstat_error}") print("✅ Python socket replacement works correctly") print("✅ Test confirms both the problem and the solution work as expected") diff --git a/tests/unit/test_ps_pkill_dependency.py b/tests/unit/test_ps_pkill_dependency.py index 61b8961..0abbfdf 100644 --- a/tests/unit/test_ps_pkill_dependency.py +++ b/tests/unit/test_ps_pkill_dependency.py @@ -53,7 +53,9 @@ async def test_bundle_cleanup_without_ps_pkill(): pkill_failed = False pkill_error = None try: - pkill_result = subprocess.run(["pkill", "-f", "sbctl"], capture_output=True, text=True) + pkill_result = subprocess.run( + ["pkill", "-f", "sbctl"], capture_output=True, text=True + ) pytest.fail( f"❌ TDD PROBLEM: pkill command was found and executed successfully!\n" f"Return code: {pkill_result.returncode}\n" @@ -131,11 +133,15 @@ def terminate_processes_by_name(process_name: str) -> int: python_processes = find_processes_by_name("python") # Should find at least this test process - assert len(python_processes) >= 1, "Should find at least the current python process" + assert ( + len(python_processes) >= 1 + ), "Should find at least the current python process" # Verify the returned objects are psutil Process instances for proc in python_processes: - assert isinstance(proc, psutil.Process), "Should return psutil Process objects" + assert isinstance( + proc, psutil.Process + ), "Should return psutil Process objects" # Test that we can access process information current_process = psutil.Process() # Current process @@ -147,7 +153,9 @@ def terminate_processes_by_name(process_name: str) -> int: # Test termination function (but don't actually terminate anything) # Just verify the function structure works without causing harm - fake_process_count = terminate_processes_by_name("nonexistent_process_name_12345") + fake_process_count = terminate_processes_by_name( + "nonexistent_process_name_12345" + ) assert fake_process_count == 0, "Should return 0 for non-existent processes" # SUCCESS: The psutil-based implementation works correctly @@ -251,7 +259,9 @@ def test_psutil_availability(): # Test process filtering functionality python_procs = [ - p for p in psutil.process_iter(["name"]) if "python" in p.info["name"].lower() + p + for p in psutil.process_iter(["name"]) + if "python" in p.info["name"].lower() ] assert len(python_procs) >= 1, "Should find at least one python process" @@ -261,7 +271,9 @@ def test_psutil_availability(): print(f"✅ Found {len(python_procs)} python processes") except ImportError: - pytest.fail("❌ psutil is not available! Add psutil to pyproject.toml dependencies.") + pytest.fail( + "❌ psutil is not available! Add psutil to pyproject.toml dependencies." + ) except Exception as e: pytest.fail( f"❌ psutil functionality test failed: {str(e)}\nError type: {type(e).__name__}" diff --git a/tests/unit/test_python313_transport_issue.py b/tests/unit/test_python313_transport_issue.py index 49564ed..84ddba2 100644 --- a/tests/unit/test_python313_transport_issue.py +++ b/tests/unit/test_python313_transport_issue.py @@ -41,7 +41,9 @@ async def test_subprocess_transport_cleanup_triggers_error(): # Look for Python 3.13 compatibility checks has_python313_check = "3.13" in source or "version_info" in source has_transport_cleanup = "_closing" in source or "transport" in source.lower() - has_pipe_transport_fix = "UnixReadPipeTransport" in source or "pipe_transport" in source + has_pipe_transport_fix = ( + "UnixReadPipeTransport" in source or "pipe_transport" in source + ) # Current subprocess_utils should NOT have these fixes yet (for TDD) if has_python313_check and has_transport_cleanup and has_pipe_transport_fix: @@ -75,7 +77,9 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): returncode, stdout, stderr = await subprocess_exec_with_cleanup( "echo", f"transport_stress_test_{i}", timeout=5.0 ) - assert returncode == 0, f"subprocess_exec_with_cleanup failed at iteration {i}" + assert ( + returncode == 0 + ), f"subprocess_exec_with_cleanup failed at iteration {i}" # Force garbage collection aggressively for _ in range(15): @@ -145,7 +149,9 @@ def error_capture(message, category, filename, lineno, file=None, line=None): returncode, stdout, stderr = await subprocess_exec_with_cleanup( "echo", f"subprocess_utils_test_{i}", timeout=5.0 ) - assert returncode == 0, f"subprocess_exec_with_cleanup failed: {stderr.decode()}" + assert ( + returncode == 0 + ), f"subprocess_exec_with_cleanup failed: {stderr.decode()}" # Force garbage collection to trigger transport cleanup for _ in range(10): @@ -264,7 +270,9 @@ def patched_collect(): print("✅ Transport cleanup fixes appear to be handling the issue") # Test passes - even if some transports are live, no errors occurred else: - print("✅ TDD SUCCESS: No _closing attribute errors and no leaked transports!") + print( + "✅ TDD SUCCESS: No _closing attribute errors and no leaked transports!" + ) print("✅ Python 3.13 transport cleanup fixes are working correctly") print("✅ Transport lifecycle is properly managed") # Test passes - perfect cleanup with no issues diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index e0a399c..48f9026 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -83,11 +83,15 @@ async def test_initialize_bundle_tool(tmp_path: Path) -> None: patch.object( bundle_manager, "_check_sbctl_available", new_callable=AsyncMock ) as mock_sbctl, - patch.object(bundle_manager, "initialize_bundle", new_callable=AsyncMock) as mock_init, + patch.object( + bundle_manager, "initialize_bundle", new_callable=AsyncMock + ) as mock_init, patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object(bundle_manager, "get_diagnostic_info", new_callable=AsyncMock) as mock_diag, + patch.object( + bundle_manager, "get_diagnostic_info", new_callable=AsyncMock + ) as mock_diag, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, ): # Set up mocks for external dependencies only @@ -100,7 +104,9 @@ async def test_initialize_bundle_tool(tmp_path: Path) -> None: # Create InitializeBundleArgs instance from mcp_server_troubleshoot.bundle import InitializeBundleArgs - args = InitializeBundleArgs(source=str(temp_source_file), force=False, verbosity="verbose") + args = InitializeBundleArgs( + source=str(temp_source_file), force=False, verbosity="verbose" + ) # Call the tool function directly response = await initialize_bundle(args) @@ -164,9 +170,15 @@ async def test_kubectl_tool(tmp_path: Path) -> None: patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object(kubectl_executor, "execute", new_callable=AsyncMock) as mock_execute, - patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, - patch("mcp_server_troubleshoot.server.get_kubectl_executor") as mock_get_executor, + patch.object( + kubectl_executor, "execute", new_callable=AsyncMock + ) as mock_execute, + patch( + "mcp_server_troubleshoot.server.get_bundle_manager" + ) as mock_get_manager, + patch( + "mcp_server_troubleshoot.server.get_kubectl_executor" + ) as mock_get_executor, ): # Set up mocks for external dependencies only mock_api.return_value = True @@ -231,7 +243,9 @@ async def test_kubectl_tool_host_only_bundle(tmp_path: Path) -> None: with ( patch.object(bundle_manager, "get_active_bundle", return_value=mock_bundle), - patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, + patch( + "mcp_server_troubleshoot.server.get_bundle_manager" + ) as mock_get_manager, ): mock_get_manager.return_value = bundle_manager @@ -298,14 +312,18 @@ async def test_file_operations(tmp_path: Path) -> None: with ( patch.object(bundle_manager, "get_active_bundle", return_value=mock_bundle), - patch("mcp_server_troubleshoot.server.get_file_explorer") as mock_get_explorer, + patch( + "mcp_server_troubleshoot.server.get_file_explorer" + ) as mock_get_explorer, ): mock_get_explorer.return_value = file_explorer # 1. Test list_files with real files from mcp_server_troubleshoot.files import ListFilesArgs - list_args = ListFilesArgs(path="test_data/dir1", recursive=False, verbosity="verbose") + list_args = ListFilesArgs( + path="test_data/dir1", recursive=False, verbosity="verbose" + ) list_response = await list_files(list_args) # Verify the response contains real file information diff --git a/tests/unit/test_server_parametrized.py b/tests/unit/test_server_parametrized.py index 692e304..b48ef5d 100644 --- a/tests/unit/test_server_parametrized.py +++ b/tests/unit/test_server_parametrized.py @@ -145,11 +145,15 @@ async def test_initialize_bundle_tool_parametrized( patch.object( bundle_manager, "_check_sbctl_available", new_callable=AsyncMock ) as mock_sbctl, - patch.object(bundle_manager, "initialize_bundle", new_callable=AsyncMock) as mock_init, + patch.object( + bundle_manager, "initialize_bundle", new_callable=AsyncMock + ) as mock_init, patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object(bundle_manager, "get_diagnostic_info", new_callable=AsyncMock) as mock_diag, + patch.object( + bundle_manager, "get_diagnostic_info", new_callable=AsyncMock + ) as mock_diag, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, ): # Set up mocks for external dependencies only @@ -162,7 +166,9 @@ async def test_initialize_bundle_tool_parametrized( # Create InitializeBundleArgs instance from mcp_server_troubleshoot.bundle import InitializeBundleArgs - args = InitializeBundleArgs(source=str(temp_source_file), force=force, verbosity="verbose") + args = InitializeBundleArgs( + source=str(temp_source_file), force=force, verbosity="verbose" + ) # Call the tool function response = await initialize_bundle(args) @@ -272,10 +278,16 @@ async def test_kubectl_tool_parametrized( patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object(bundle_manager, "get_diagnostic_info", new_callable=AsyncMock) as mock_diag, - patch.object(kubectl_executor, "execute", new_callable=AsyncMock) as mock_execute, + patch.object( + bundle_manager, "get_diagnostic_info", new_callable=AsyncMock + ) as mock_diag, + patch.object( + kubectl_executor, "execute", new_callable=AsyncMock + ) as mock_execute, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, - patch("mcp_server_troubleshoot.server.get_kubectl_executor") as mock_get_executor, + patch( + "mcp_server_troubleshoot.server.get_kubectl_executor" + ) as mock_get_executor, ): # Set up mocks mock_api.return_value = True @@ -643,16 +655,22 @@ async def test_file_operations_error_handling( ) # 1. Test list_files - list_args = ListFilesArgs(path="test/path", recursive=False, verbosity="verbose") + list_args = ListFilesArgs( + path="test/path", recursive=False, verbosity="verbose" + ) list_response = await list_files(list_args) - test_assertions.assert_api_response_valid(list_response, "text", expected_strings) + test_assertions.assert_api_response_valid( + list_response, "text", expected_strings + ) # 2. Test read_file read_args = ReadFileArgs( path="test/file.txt", start_line=0, end_line=None, verbosity="verbose" ) read_response = await read_file(read_args) - test_assertions.assert_api_response_valid(read_response, "text", expected_strings) + test_assertions.assert_api_response_valid( + read_response, "text", expected_strings + ) # 3. Test grep_files grep_args = GrepFilesArgs( @@ -667,7 +685,9 @@ async def test_file_operations_error_handling( verbosity="verbose", ) grep_response = await grep_files(grep_args) - test_assertions.assert_api_response_valid(grep_response, "text", expected_strings) + test_assertions.assert_api_response_valid( + grep_response, "text", expected_strings + ) # No manual cleanup needed - tmp_path handles it automatically @@ -767,7 +787,9 @@ class MockAvailableBundle: # Mock only the list_available_bundles method, keep rest of BundleManager real with ( - patch.object(bundle_manager, "list_available_bundles", new_callable=AsyncMock) as mock_list, + patch.object( + bundle_manager, "list_available_bundles", new_callable=AsyncMock + ) as mock_list, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, ): mock_list.return_value = bundles @@ -776,7 +798,9 @@ class MockAvailableBundle: # Create ListAvailableBundlesArgs instance from mcp_server_troubleshoot.bundle import ListAvailableBundlesArgs - args = ListAvailableBundlesArgs(include_invalid=include_invalid, verbosity="verbose") + args = ListAvailableBundlesArgs( + include_invalid=include_invalid, verbosity="verbose" + ) # Call the tool function response = await list_available_bundles(args) diff --git a/tests/unit/test_size_limiter.py b/tests/unit/test_size_limiter.py index c071b63..b835adc 100644 --- a/tests/unit/test_size_limiter.py +++ b/tests/unit/test_size_limiter.py @@ -79,7 +79,9 @@ def test_size_limit_thresholds(token_count, limit, should_pass): (None, 25000), # Default when not set ], ) -def test_mcp_token_limit_environment_variable(mock_environment, env_value, expected_limit): +def test_mcp_token_limit_environment_variable( + mock_environment, env_value, expected_limit +): """ Test MCP_TOKEN_LIMIT environment variable configuration. """ @@ -98,7 +100,9 @@ def test_mcp_token_limit_environment_variable(mock_environment, env_value, expec (None, True), # Default when not set ], ) -def test_mcp_size_check_enabled_environment_variable(mock_environment, env_value, expected_enabled): +def test_mcp_size_check_enabled_environment_variable( + mock_environment, env_value, expected_enabled +): """ Test MCP_SIZE_CHECK_ENABLED environment variable configuration. """ diff --git a/tests/unit/test_transport_cleanup_reproduction.py b/tests/unit/test_transport_cleanup_reproduction.py index 058b7e5..301daea 100644 --- a/tests/unit/test_transport_cleanup_reproduction.py +++ b/tests/unit/test_transport_cleanup_reproduction.py @@ -186,7 +186,9 @@ async def test_subprocess_transport_cleanup_single_operation( @pytest.mark.asyncio -async def test_subprocess_transport_cleanup_multiple_operations(clean_asyncio, transport_detector): +async def test_subprocess_transport_cleanup_multiple_operations( + clean_asyncio, transport_detector +): """ Test transport cleanup with multiple rapid subprocess operations. @@ -216,7 +218,9 @@ async def test_subprocess_transport_cleanup_multiple_operations(clean_asyncio, t @pytest.mark.asyncio -async def test_concurrent_subprocess_transport_cleanup(clean_asyncio, transport_detector): +async def test_concurrent_subprocess_transport_cleanup( + clean_asyncio, transport_detector +): """ Test transport cleanup with concurrent subprocess operations. @@ -226,7 +230,9 @@ async def test_concurrent_subprocess_transport_cleanup(clean_asyncio, transport_ # Create multiple concurrent subprocess operations tasks = [] for i in range(3): - task = asyncio.create_task(create_subprocess_and_wait(["echo", f"concurrent_{i}"])) + task = asyncio.create_task( + create_subprocess_and_wait(["echo", f"concurrent_{i}"]) + ) tasks.append(task) # Wait for all tasks to complete @@ -334,7 +340,9 @@ async def test_subprocess_pattern_like_curl(clean_asyncio, transport_detector): @pytest.mark.asyncio -async def test_transport_cleanup_with_process_termination(clean_asyncio, transport_detector): +async def test_transport_cleanup_with_process_termination( + clean_asyncio, transport_detector +): """ Test transport cleanup when processes are forcibly terminated. @@ -383,7 +391,9 @@ async def test_event_loop_with_many_transports(clean_asyncio, transport_detector # Create many subprocess operations to stress test transport cleanup tasks = [] for i in range(8): # Reasonable number to avoid overwhelming the system - task = asyncio.create_task(create_subprocess_and_wait(["echo", f"stress_test_{i}"])) + task = asyncio.create_task( + create_subprocess_and_wait(["echo", f"stress_test_{i}"]) + ) tasks.append(task) # Wait for all to complete @@ -483,7 +493,9 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): if captured_warnings: print(f"\nCaptured {len(captured_warnings)} warnings:") for w in captured_warnings: - print(f" {w['category']}: {w['message']} ({w['filename']}:{w['lineno']})") + print( + f" {w['category']}: {w['message']} ({w['filename']}:{w['lineno']})" + ) if transport_warnings: print(f"\nFound {len(transport_warnings)} transport-related warnings:") @@ -531,7 +543,8 @@ async def test_aggressive_subprocess_cleanup_stress(): def capture_warnings(message, category, filename, lineno, file=None, line=None): msg_str = str(message) if any( - keyword in msg_str.lower() for keyword in ["transport", "unclosed", "_closing", "pipe"] + keyword in msg_str.lower() + for keyword in ["transport", "unclosed", "_closing", "pipe"] ): captured_issues.append(f"{category.__name__}: {msg_str}") @@ -619,7 +632,9 @@ async def test_simulate_unix_read_pipe_transport_missing_closing_attribute(): def mock_transport_repr_error(): """Simulate the _closing attribute missing error during __repr__""" - raise AttributeError("'_UnixReadPipeTransport' object has no attribute '_closing'") + raise AttributeError( + "'_UnixReadPipeTransport' object has no attribute '_closing'" + ) try: # Remove warning filters to see all warnings diff --git a/tests/util/debug_mcp.py b/tests/util/debug_mcp.py index e1329f6..e261975 100755 --- a/tests/util/debug_mcp.py +++ b/tests/util/debug_mcp.py @@ -83,7 +83,9 @@ def read_stderr(): if response_line: try: response = json.loads(response_line.decode("utf-8")) - print(f"Received JSON-RPC response: {json.dumps(response, indent=2)}") + print( + f"Received JSON-RPC response: {json.dumps(response, indent=2)}" + ) except json.JSONDecodeError as e: print(f"Failed to decode response as JSON: {e}") else: From c0e488b08af498263959769c8cc62408a3ef806b Mon Sep 17 00:00:00 2001 From: Chris Sanders Date: Mon, 28 Jul 2025 18:28:14 -0500 Subject: [PATCH 5/6] Update task progress with PR information --- tasks/active/fix-container-name-conflicts.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tasks/active/fix-container-name-conflicts.md b/tasks/active/fix-container-name-conflicts.md index ef18ccd..7bbd8dc 100644 --- a/tasks/active/fix-container-name-conflicts.md +++ b/tasks/active/fix-container-name-conflicts.md @@ -7,7 +7,10 @@ **Started**: 2025-07-28 ## Progress Log -- 2025-07-28: Started task, created worktree, began implementation +- 2025-07-28: Started task, created worktree, began implementation +- 2025-07-28: Completed core changes - updated build script and test harnesses +- 2025-07-28: Updated documentation (PODMAN.md, README.md, TESTING_STRATEGY.md) +- 2025-07-28: All tests passing, quality checks passed, PR created: #46 ## Problem Statement From 1f3cb4302625a25a23b304326e4e5e249c716a1d Mon Sep 17 00:00:00 2001 From: Chris Sanders Date: Mon, 28 Jul 2025 18:29:32 -0500 Subject: [PATCH 6/6] Fix formatting issues with ruff format Use ruff format instead of black to match CI expectations --- debug_fastmcp_lifecycle.py | 4 +- debug_mcp_server.py | 4 +- debug_sbctl.py | 16 +- simple_mcp_test.py | 4 +- src/mcp_server_troubleshoot/__main__.py | 12 +- src/mcp_server_troubleshoot/bundle.py | 321 +++++------------- src/mcp_server_troubleshoot/cli.py | 4 +- src/mcp_server_troubleshoot/files.py | 48 +-- src/mcp_server_troubleshoot/formatters.py | 71 ++-- src/mcp_server_troubleshoot/kubectl.py | 28 +- src/mcp_server_troubleshoot/lifecycle.py | 16 +- src/mcp_server_troubleshoot/server.py | 17 +- .../subprocess_utils.py | 16 +- test_bundle_loading.py | 8 +- test_initialize_bundle_tool.py | 12 +- test_mcp_communication.py | 12 +- test_minimal_mcp.py | 4 +- test_module_startup.py | 8 +- test_production_server.py | 8 +- test_sbctl_direct.py | 8 +- test_simple_mcp.py | 8 +- tests/conftest.py | 12 +- tests/e2e/test_build_reliability.py | 34 +- tests/e2e/test_container_bundle_validation.py | 16 +- .../test_container_production_validation.py | 22 +- .../test_container_shutdown_reliability.py | 8 +- tests/e2e/test_direct_tool_integration.py | 49 +-- tests/e2e/test_mcp_protocol_integration.py | 156 +++------ tests/e2e/test_non_container.py | 36 +- tests/e2e/test_podman.py | 16 +- tests/integration/conftest.py | 14 +- tests/integration/mcp_test_utils.py | 19 +- .../integration/test_api_server_lifecycle.py | 42 +-- tests/integration/test_mcp_protocol_errors.py | 4 +- tests/integration/test_real_bundle.py | 98 ++---- tests/integration/test_server_lifecycle.py | 20 +- .../test_shutdown_race_condition.py | 8 +- .../test_signal_handling_integration.py | 10 +- .../test_subprocess_utilities_integration.py | 20 +- tests/integration/test_tool_functions.py | 30 +- tests/integration/test_url_fetch_auth.py | 32 +- tests/test_all.py | 4 +- tests/test_utils/bundle_helpers.py | 12 +- tests/unit/conftest.py | 18 +- tests/unit/test_bundle.py | 112 ++---- .../unit/test_bundle_cleanup_dependencies.py | 8 +- tests/unit/test_components.py | 40 +-- .../unit/test_curl_dependency_reproduction.py | 22 +- tests/unit/test_files.py | 80 ++--- tests/unit/test_files_parametrized.py | 4 +- tests/unit/test_fixes_integration.py | 96 ++---- tests/unit/test_grep_fix.py | 12 +- tests/unit/test_kubectl.py | 56 +-- tests/unit/test_kubectl_parametrized.py | 20 +- tests/unit/test_lifecycle.py | 8 +- tests/unit/test_list_bundles.py | 16 +- tests/unit/test_netstat_dependency.py | 16 +- tests/unit/test_ps_pkill_dependency.py | 24 +- tests/unit/test_python313_transport_issue.py | 16 +- tests/unit/test_server.py | 36 +- tests/unit/test_server_parametrized.py | 48 +-- tests/unit/test_size_limiter.py | 8 +- .../test_transport_cleanup_reproduction.py | 31 +- tests/util/debug_mcp.py | 4 +- 64 files changed, 564 insertions(+), 1402 deletions(-) diff --git a/debug_fastmcp_lifecycle.py b/debug_fastmcp_lifecycle.py index 54529ae..f93f754 100644 --- a/debug_fastmcp_lifecycle.py +++ b/debug_fastmcp_lifecycle.py @@ -143,9 +143,7 @@ async def test_server_communication(): # Try to get response with short timeout try: if process.stdout: - response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=5.0 - ) + response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) response_line = response_bytes.decode().strip() print(f"🔍 DEBUG: Got response: {response_line}") except asyncio.TimeoutError: diff --git a/debug_mcp_server.py b/debug_mcp_server.py index e020c65..6713efe 100644 --- a/debug_mcp_server.py +++ b/debug_mcp_server.py @@ -115,9 +115,7 @@ async def send_request_debug(process, request, timeout=5.0): # Try to get response try: if process.stdout: - response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=timeout - ) + response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=timeout) response_line = response_bytes.decode().strip() print(f"Response: {response_line[:200]}...") diff --git a/debug_sbctl.py b/debug_sbctl.py index 8903d49..2045fe0 100644 --- a/debug_sbctl.py +++ b/debug_sbctl.py @@ -113,9 +113,7 @@ async def debug_sbctl(): # Try to read any output try: if process.stdout: - stdout_data = await asyncio.wait_for( - process.stdout.read(), timeout=1.0 - ) + stdout_data = await asyncio.wait_for(process.stdout.read(), timeout=1.0) if stdout_data: print(f"STDOUT: {stdout_data.decode()}") except asyncio.TimeoutError: @@ -123,9 +121,7 @@ async def debug_sbctl(): try: if process.stderr: - stderr_data = await asyncio.wait_for( - process.stderr.read(), timeout=1.0 - ) + stderr_data = await asyncio.wait_for(process.stderr.read(), timeout=1.0) if stderr_data: print(f"STDERR: {stderr_data.decode()}") except asyncio.TimeoutError: @@ -133,9 +129,7 @@ async def debug_sbctl(): # Check what files were created files_created = list(temp_dir_path.glob("*")) - print( - f"\nFiles created in temp directory: {[f.name for f in files_created]}" - ) + print(f"\nFiles created in temp directory: {[f.name for f in files_created]}") # Check if kubeconfig was created kubeconfig_path = temp_dir_path / "kubeconfig" @@ -144,9 +138,7 @@ async def debug_sbctl(): try: with open(kubeconfig_path, "r") as f: content = f.read() - print( - f"Kubeconfig content ({len(content)} chars):\n{content[:500]}..." - ) + print(f"Kubeconfig content ({len(content)} chars):\n{content[:500]}...") except Exception as e: print(f"Error reading kubeconfig: {e}") else: diff --git a/simple_mcp_test.py b/simple_mcp_test.py index 5c39c0a..55b844d 100644 --- a/simple_mcp_test.py +++ b/simple_mcp_test.py @@ -78,9 +78,7 @@ def test_mcp_server(): import select # Use select to wait for output with timeout - ready, _, _ = select.select( - [process.stdout], [], [], 5.0 - ) # 5 second timeout + ready, _, _ = select.select([process.stdout], [], [], 5.0) # 5 second timeout if ready: response = process.stdout.readline() diff --git a/src/mcp_server_troubleshoot/__main__.py b/src/mcp_server_troubleshoot/__main__.py index 69c0224..45f61ca 100644 --- a/src/mcp_server_troubleshoot/__main__.py +++ b/src/mcp_server_troubleshoot/__main__.py @@ -75,15 +75,9 @@ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: Returns: Parsed arguments """ - parser = argparse.ArgumentParser( - description="MCP server for Kubernetes support bundles" - ) - parser.add_argument( - "--verbose", "-v", action="store_true", help="Enable verbose logging" - ) - parser.add_argument( - "--bundle-dir", type=str, help="Directory to store support bundles" - ) + parser = argparse.ArgumentParser(description="MCP server for Kubernetes support bundles") + parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") + parser.add_argument("--bundle-dir", type=str, help="Directory to store support bundles") parser.add_argument( "--show-config", action="store_true", diff --git a/src/mcp_server_troubleshoot/bundle.py b/src/mcp_server_troubleshoot/bundle.py index de35809..e4fed3e 100644 --- a/src/mcp_server_troubleshoot/bundle.py +++ b/src/mcp_server_troubleshoot/bundle.py @@ -35,15 +35,11 @@ # Feature flags - can be enabled/disabled via environment variables DEFAULT_CLEANUP_ORPHANED = True # Clean up orphaned sbctl processes -DEFAULT_ALLOW_ALTERNATIVE_KUBECONFIG = ( - True # Allow finding kubeconfig in alternative locations -) +DEFAULT_ALLOW_ALTERNATIVE_KUBECONFIG = True # Allow finding kubeconfig in alternative locations # Override with environment variables if provided MAX_DOWNLOAD_SIZE = int(os.environ.get("MAX_DOWNLOAD_SIZE", DEFAULT_DOWNLOAD_SIZE)) -MAX_DOWNLOAD_TIMEOUT = int( - os.environ.get("MAX_DOWNLOAD_TIMEOUT", DEFAULT_DOWNLOAD_TIMEOUT) -) +MAX_DOWNLOAD_TIMEOUT = int(os.environ.get("MAX_DOWNLOAD_TIMEOUT", DEFAULT_DOWNLOAD_TIMEOUT)) MAX_INITIALIZATION_TIMEOUT = int( os.environ.get("MAX_INITIALIZATION_TIMEOUT", DEFAULT_INITIALIZATION_TIMEOUT) ) @@ -84,9 +80,7 @@ def safe_copy_file(src: Union[Path, None], dst: Union[Path, None]) -> None: logger.debug(f"Using MAX_DOWNLOAD_TIMEOUT: {MAX_DOWNLOAD_TIMEOUT} seconds") logger.debug(f"Using MAX_INITIALIZATION_TIMEOUT: {MAX_INITIALIZATION_TIMEOUT} seconds") logger.debug(f"Feature flags - Cleanup orphaned processes: {CLEANUP_ORPHANED}") -logger.debug( - f"Feature flags - Allow alternative kubeconfig: {ALLOW_ALTERNATIVE_KUBECONFIG}" -) +logger.debug(f"Feature flags - Allow alternative kubeconfig: {ALLOW_ALTERNATIVE_KUBECONFIG}") # Constants for Replicated Vendor Portal integration REPLICATED_VENDOR_URL_PATTERN = re.compile( @@ -105,9 +99,7 @@ class BundleMetadata(BaseModel): source: str = Field(description="The source of the bundle (URL or local path)") path: Path = Field(description="The path to the extracted bundle") kubeconfig_path: Path = Field(description="The path to the kubeconfig file") - initialized: bool = Field( - description="Whether the bundle has been initialized with sbctl" - ) + initialized: bool = Field(description="Whether the bundle has been initialized with sbctl") host_only_bundle: bool = Field( False, description="Whether this bundle contains only host resources (no cluster resources)", @@ -210,17 +202,13 @@ class BundleFileInfo(BaseModel): """ path: str = Field(description="The full path to the bundle file") - relative_path: str = Field( - description="The relative path without bundle directory prefix" - ) + relative_path: str = Field(description="The relative path without bundle directory prefix") name: str = Field(description="The name of the bundle file") size_bytes: int = Field(description="The size of the bundle file in bytes") modified_time: float = Field( description="The modification time of the bundle file (seconds since epoch)" ) - valid: bool = Field( - description="Whether the bundle appears to be a valid support bundle" - ) + valid: bool = Field(description="Whether the bundle appears to be a valid support bundle") validation_message: Optional[str] = Field( None, description="Message explaining why the bundle is invalid, if applicable" ) @@ -250,9 +238,7 @@ def __init__(self, bundle_dir: Optional[Path] = None) -> None: self._host_only_bundle: bool = False self._termination_requested: bool = False - async def initialize_bundle( - self, source: str, force: bool = False - ) -> BundleMetadata: + async def initialize_bundle(self, source: str, force: bool = False) -> BundleMetadata: """ Initialize a support bundle from a source. @@ -295,19 +281,14 @@ async def initialize_bundle( include_invalid=True ) for bundle in available_bundles: - if ( - bundle.relative_path == source - or bundle.name == source - ): + if bundle.relative_path == source or bundle.name == source: logger.info( f"Found matching bundle by relative path: {bundle.path}" ) bundle_path = Path(bundle.path) break except Exception as e: - logger.warning( - f"Error searching for bundle by relative path: {e}" - ) + logger.warning(f"Error searching for bundle by relative path: {e}") # If we still can't find it, raise an error if not bundle_path.exists(): @@ -323,9 +304,7 @@ async def initialize_bundle( bundle_output_dir.mkdir(parents=True, exist_ok=True) # Initialize the bundle with sbctl - kubeconfig_path = await self._initialize_with_sbctl( - bundle_path, bundle_output_dir - ) + kubeconfig_path = await self._initialize_with_sbctl(bundle_path, bundle_output_dir) # Handle case where no kubeconfig was created (host-only bundle) if self._host_only_bundle: @@ -367,9 +346,7 @@ async def initialize_bundle( with tarfile.open(bundle_path, "r:gz") as tar: # First list the files to get a count members = tar.getmembers() - logger.info( - f"Support bundle contains {len(members)} entries" - ) + logger.info(f"Support bundle contains {len(members)} entries") # Extract all files from pathlib import PurePath @@ -384,9 +361,7 @@ async def initialize_bundle( # Extract with the sanitized member list # Use filter='data' to only extract file data without modifying metadata - tar.extractall( - path=extract_dir, members=safe_members, filter="data" - ) + tar.extractall(path=extract_dir, members=safe_members, filter="data") # List extracted files and verify extraction was successful file_count = 0 @@ -467,9 +442,7 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: # Process the response status and content if response.status_code == 401: - logger.error( - f"Replicated API returned 401 Unauthorized for slug {slug}" - ) + logger.error(f"Replicated API returned 401 Unauthorized for slug {slug}") raise BundleDownloadError( f"Failed to authenticate with Replicated API (status {response.status_code}). " "Check SBCTL_TOKEN/REPLICATED_TOKEN." @@ -496,9 +469,7 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: logger.exception( f"Error decoding JSON response from Replicated API (status 200): {json_e}" ) - raise BundleDownloadError( - f"Invalid JSON response from Replicated API: {json_e}" - ) + raise BundleDownloadError(f"Invalid JSON response from Replicated API: {json_e}") # Add validation: Ensure response_data is a dictionary if not isinstance(response_data, dict): @@ -529,9 +500,7 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: logger.error( f"Missing 'signedUri' in Replicated API response bundle object for slug {slug}. Bundle data: {bundle_data}" ) - raise BundleDownloadError( - "Could not find 'signedUri' in Replicated API response." - ) + raise BundleDownloadError("Could not find 'signedUri' in Replicated API response.") logger.info("Successfully retrieved signed URL from Replicated API.") # Ensure we're returning a string type @@ -544,18 +513,12 @@ async def _get_replicated_signed_url(self, original_url: str) -> str: # Re-raise specific BundleDownloadErrors we've already identified raise e elif isinstance(e, httpx.Timeout): - logger.exception( - f"Timeout requesting signed URL from Replicated API: {e}" - ) + logger.exception(f"Timeout requesting signed URL from Replicated API: {e}") raise BundleDownloadError(f"Timeout requesting signed URL: {e}") from e elif isinstance(e, httpx.RequestError): # This should now correctly catch the RequestError raised by the mock - logger.exception( - f"Network error requesting signed URL from Replicated API: {e}" - ) - raise BundleDownloadError( - f"Network error requesting signed URL: {e}" - ) from e + logger.exception(f"Network error requesting signed URL from Replicated API: {e}") + raise BundleDownloadError(f"Network error requesting signed URL: {e}") from e else: # Catch any other unexpected errors during the entire process and wrap them distinct_error_msg = f"UNEXPECTED EXCEPTION in _get_replicated_signed_url: {type(e).__name__}: {str(e)}" @@ -596,9 +559,7 @@ async def _download_bundle(self, url: str) -> Path: # Catch any other unexpected errors during signed URL retrieval logger.exception(f"Unexpected error getting signed URL for {url}: {e}") # Raise specific error and exit - raise BundleDownloadError( - f"Failed to get signed URL for {url}: {str(e)}" - ) + raise BundleDownloadError(f"Failed to get signed URL for {url}: {str(e)}") # Log the download start *after* potential signed URL retrieval logger.info(f"Starting download from: {actual_download_url[:80]}...") @@ -630,9 +591,7 @@ async def _download_bundle(self, url: str) -> Path: # Headers for the actual download download_headers = {} # Add auth token ONLY for non-Replicated URLs (signed URLs have auth embedded) - if ( - actual_download_url == original_url - ): # Check if we are using the original URL + if actual_download_url == original_url: # Check if we are using the original URL token = os.environ.get("SBCTL_TOKEN") if token: download_headers["Authorization"] = f"Bearer {token}" @@ -648,9 +607,7 @@ async def _download_bundle(self, url: str) -> Path: async with aiohttp.ClientSession(timeout=timeout) as session: # === START MODIFICATION === # Explicitly await the get call first - response_ctx_mgr = session.get( - actual_download_url, headers=download_headers - ) + response_ctx_mgr = session.get(actual_download_url, headers=download_headers) # Now use the awaited response object in the async with async with await response_ctx_mgr as response: # === END MODIFICATION === @@ -697,19 +654,13 @@ async def _download_bundle(self, url: str) -> Path: return download_path except Exception as e: # Use original_url in error messages for clarity - logger.exception( - f"Error downloading bundle originally from {original_url}: {str(e)}" - ) + logger.exception(f"Error downloading bundle originally from {original_url}: {str(e)}") if download_path.exists(): - download_path.unlink( - missing_ok=True - ) # Use missing_ok=True for robustness + download_path.unlink(missing_ok=True) # Use missing_ok=True for robustness # Re-raise BundleDownloadError if it's already that type if isinstance(e, BundleDownloadError): raise - raise BundleDownloadError( - f"Failed to download bundle from {original_url}: {str(e)}" - ) + raise BundleDownloadError(f"Failed to download bundle from {original_url}: {str(e)}") async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> Path: """ @@ -795,9 +746,7 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P logger.info(f"sbctl output: {all_output}") if "No cluster resources found in bundle" in all_output: - logger.info( - "Bundle contains no cluster resources, marking as host-only bundle" - ) + logger.info("Bundle contains no cluster resources, marking as host-only bundle") self._host_only_bundle = True return kubeconfig_path # Return dummy path, file won't exist but that's OK @@ -830,9 +779,7 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P ) if not line: # EOF break - line_text = line.decode( - "utf-8", errors="replace" - ).strip() + line_text = line.decode("utf-8", errors="replace").strip() if line_text: stdout_lines.append(line_text) logger.debug(f"sbctl stdout line: {line_text}") @@ -845,17 +792,13 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P r"export KUBECONFIG=([^\s]+)", line_text ) if kubeconfig_matches: - announced_kubeconfig = Path( - kubeconfig_matches[0] - ) + announced_kubeconfig = Path(kubeconfig_matches[0]) logger.info( f"sbctl announced kubeconfig at: {announced_kubeconfig}" ) # Wait a brief moment for the file to be created - for wait_attempt in range( - 10 - ): # Wait up to 5 seconds + for wait_attempt in range(10): # Wait up to 5 seconds await asyncio.sleep(0.5) if announced_kubeconfig.exists(): logger.info( @@ -881,17 +824,13 @@ async def _initialize_with_sbctl(self, bundle_path: Path, output_dir: Path) -> P break if stdout_lines: - logger.debug( - f"sbctl initial stdout: {' | '.join(stdout_lines)}" - ) + logger.debug(f"sbctl initial stdout: {' | '.join(stdout_lines)}") except Exception as read_err: logger.debug(f"Error reading initial stdout: {read_err}") # Continue with normal initialization - logger.debug( - "sbctl process continuing, proceeding with normal initialization" - ) + logger.debug("sbctl process continuing, proceeding with normal initialization") # Wait for initialization to complete await self._wait_for_initialization(kubeconfig_path) @@ -963,11 +902,7 @@ async def _wait_for_initialization( alternative_kubeconfig_paths = [] # Attempt to read process output for diagnostic purposes - if ( - self.sbctl_process - and self.sbctl_process.stdout - and self.sbctl_process.stderr - ): + if self.sbctl_process and self.sbctl_process.stdout and self.sbctl_process.stderr: stdout_data = b"" stderr_data = b"" @@ -1009,21 +944,15 @@ async def _wait_for_initialization( # Extract the kubeconfig path import re - kubeconfig_matches = re.findall( - r"export KUBECONFIG=([^\s]+)", stdout_text - ) + kubeconfig_matches = re.findall(r"export KUBECONFIG=([^\s]+)", stdout_text) if kubeconfig_matches: alt_kubeconfig = Path(kubeconfig_matches[0]) - logger.info( - f"Found kubeconfig path in stdout: {alt_kubeconfig}" - ) + logger.info(f"Found kubeconfig path in stdout: {alt_kubeconfig}") alternative_kubeconfig_paths.append(alt_kubeconfig) # Since we found the kubeconfig path immediately, use it if alt_kubeconfig.exists(): - logger.info( - f"Using kubeconfig from stdout: {alt_kubeconfig}" - ) + logger.info(f"Using kubeconfig from stdout: {alt_kubeconfig}") try: safe_copy_file(alt_kubeconfig, kubeconfig_path) logger.info( @@ -1088,14 +1017,10 @@ async def _wait_for_initialization( try: if self.sbctl_process.stdout: stdout_data = await self.sbctl_process.stdout.read() - process_output += stdout_data.decode( - "utf-8", errors="replace" - ) + process_output += stdout_data.decode("utf-8", errors="replace") if self.sbctl_process.stderr: stderr_data = await self.sbctl_process.stderr.read() - process_output += stderr_data.decode( - "utf-8", errors="replace" - ) + process_output += stderr_data.decode("utf-8", errors="replace") except Exception: pass @@ -1125,9 +1050,7 @@ async def _wait_for_initialization( if not kubeconfig_found and ALLOW_ALTERNATIVE_KUBECONFIG: for alt_path in alternative_kubeconfig_paths: if alt_path.exists(): - logger.info( - f"Kubeconfig found at alternative location: {alt_path}" - ) + logger.info(f"Kubeconfig found at alternative location: {alt_path}") kubeconfig_found = True kubeconfig_found_time = asyncio.get_event_loop().time() found_kubeconfig_path = alt_path @@ -1136,9 +1059,7 @@ async def _wait_for_initialization( try: with open(alt_path, "r") as f: kubeconfig_content = f.read() - logger.debug( - f"Alternative kubeconfig content:\n{kubeconfig_content}" - ) + logger.debug(f"Alternative kubeconfig content:\n{kubeconfig_content}") # Try to copy to expected location try: @@ -1149,9 +1070,7 @@ async def _wait_for_initialization( except Exception as copy_err: logger.warning(f"Failed to copy kubeconfig: {copy_err}") except Exception as e: - logger.warning( - f"Failed to read alternative kubeconfig content: {e}" - ) + logger.warning(f"Failed to read alternative kubeconfig content: {e}") break @@ -1206,9 +1125,7 @@ async def _wait_for_initialization( time_since_kubeconfig = ( asyncio.get_event_loop().time() - kubeconfig_found_time ) - if time_since_kubeconfig > ( - timeout * api_server_wait_percentage - ): + if time_since_kubeconfig > (timeout * api_server_wait_percentage): logger.warning( f"API server not responding after {time_since_kubeconfig:.1f}s " f"({api_server_wait_percentage * 100:.0f}% of timeout). Proceeding anyway." @@ -1240,9 +1157,7 @@ async def _wait_for_initialization( stdout_data = await asyncio.wait_for( self.sbctl_process.stdout.read(), timeout=1.0 ) - process_output += stdout_data.decode( - "utf-8", errors="replace" - ) + process_output += stdout_data.decode("utf-8", errors="replace") except (asyncio.TimeoutError, Exception): pass if self.sbctl_process.stderr: @@ -1250,9 +1165,7 @@ async def _wait_for_initialization( stderr_data = await asyncio.wait_for( self.sbctl_process.stderr.read(), timeout=1.0 ) - process_output += stderr_data.decode( - "utf-8", errors="replace" - ) + process_output += stderr_data.decode("utf-8", errors="replace") except (asyncio.TimeoutError, Exception): pass @@ -1279,9 +1192,7 @@ async def _wait_for_initialization( # Check if this was an intentional termination (SIGTERM/-15) if self.sbctl_process.returncode == -15 and self._termination_requested: - logger.debug( - "sbctl process was intentionally terminated during cleanup" - ) + logger.debug("sbctl process was intentionally terminated during cleanup") return # Exit gracefully without raising an error error_message = f"sbctl process exited with code {self.sbctl_process.returncode} before initialization completed" @@ -1354,9 +1265,7 @@ async def _terminate_sbctl_process(self) -> None: await asyncio.wait_for(self.sbctl_process.wait(), timeout=3.0) logger.debug("sbctl process terminated gracefully") except (asyncio.TimeoutError, ProcessLookupError) as e: - logger.warning( - f"Failed to terminate sbctl process gracefully: {str(e)}" - ) + logger.warning(f"Failed to terminate sbctl process gracefully: {str(e)}") if self.sbctl_process: try: logger.debug("Killing sbctl process...") @@ -1389,18 +1298,14 @@ async def _terminate_sbctl_process(self) -> None: # Check if process is gone os.kill(pid, 0) # If we get here, process still exists, try SIGKILL - logger.debug( - f"Process {pid} still exists, sending SIGKILL" - ) + logger.debug(f"Process {pid} still exists, sending SIGKILL") os.kill(pid, signal.SIGKILL) except ProcessLookupError: logger.debug(f"Process {pid} terminated successfully") except ProcessLookupError: logger.debug(f"Process {pid} not found") except PermissionError: - logger.warning( - f"Permission error trying to kill process {pid}" - ) + logger.warning(f"Permission error trying to kill process {pid}") # Remove the PID file try: @@ -1433,10 +1338,7 @@ async def _terminate_sbctl_process(self) -> None: proc.info["name"] and "sbctl" in proc.info["name"] and proc.info["cmdline"] - and any( - bundle_path in arg - for arg in proc.info["cmdline"] - ) + and any(bundle_path in arg for arg in proc.info["cmdline"]) ): pid = proc.info["pid"] logger.debug( @@ -1444,9 +1346,7 @@ async def _terminate_sbctl_process(self) -> None: ) try: os.kill(pid, signal.SIGTERM) - logger.debug( - f"Sent SIGTERM to process {pid}" - ) + logger.debug(f"Sent SIGTERM to process {pid}") await asyncio.sleep(0.5) # Check if terminated @@ -1465,9 +1365,7 @@ async def _terminate_sbctl_process(self) -> None: ProcessLookupError, PermissionError, ) as e: - logger.debug( - f"Error terminating process {pid}: {e}" - ) + logger.debug(f"Error terminating process {pid}: {e}") except ( psutil.NoSuchProcess, psutil.AccessDenied, @@ -1476,9 +1374,7 @@ async def _terminate_sbctl_process(self) -> None: # Process disappeared or access denied - skip it continue except Exception as e: - logger.warning( - f"Error cleaning up orphaned sbctl processes: {e}" - ) + logger.warning(f"Error cleaning up orphaned sbctl processes: {e}") # As a fallback, try to clean up any sbctl processes related to serve try: @@ -1491,9 +1387,7 @@ async def _terminate_sbctl_process(self) -> None: proc.info["name"] and "sbctl" in proc.info["name"] and proc.info["cmdline"] - and any( - "serve" in arg for arg in proc.info["cmdline"] - ) + and any("serve" in arg for arg in proc.info["cmdline"]) ): try: proc.terminate() @@ -1519,16 +1413,12 @@ async def _terminate_sbctl_process(self) -> None: else: logger.debug("No sbctl serve processes found to terminate") except Exception as e: - logger.warning( - f"Error using psutil to terminate sbctl processes: {e}" - ) + logger.warning(f"Error using psutil to terminate sbctl processes: {e}") except Exception as e: logger.warning(f"Error during extended cleanup: {e}") else: - logger.debug( - "Skipping orphaned process cleanup (disabled by configuration)" - ) + logger.debug("Skipping orphaned process cleanup (disabled by configuration)") async def _cleanup_active_bundle(self) -> None: """ @@ -1566,9 +1456,7 @@ async def _cleanup_active_bundle(self) -> None: files = glob.glob(f"{bundle_path}/**", recursive=True) logger.info(f"Found {len(files)} items in bundle directory") except Exception as list_err: - logger.warning( - f"Error getting bundle directory details: {list_err}" - ) + logger.warning(f"Error getting bundle directory details: {list_err}") # Create a list of paths we should not delete (containing parent directories) protected_paths = [ @@ -1584,9 +1472,7 @@ async def _cleanup_active_bundle(self) -> None: try: import shutil - logger.info( - f"Starting shutil.rmtree on bundle path: {bundle_path}" - ) + logger.info(f"Starting shutil.rmtree on bundle path: {bundle_path}") shutil.rmtree(bundle_path) logger.info( "shutil.rmtree completed, checking if path still exists" @@ -1601,9 +1487,7 @@ async def _cleanup_active_bundle(self) -> None: f"Successfully removed bundle directory: {bundle_path}" ) except PermissionError as e: - logger.error( - f"Permission error removing bundle directory: {e}" - ) + logger.error(f"Permission error removing bundle directory: {e}") logger.error( f"Error details: {str(e)}, file: {getattr(e, 'filename', 'unknown')}" ) @@ -1622,9 +1506,7 @@ async def _cleanup_active_bundle(self) -> None: f"Bundle path {bundle_path} is a protected path, not removing" ) if not bundle_path.exists(): - logger.warning( - f"Bundle path {bundle_path} no longer exists" - ) + logger.warning(f"Bundle path {bundle_path} no longer exists") else: if not self.active_bundle.path: logger.warning("Active bundle path is None") @@ -1671,9 +1553,7 @@ def _generate_bundle_id(self, source: str) -> str: sanitized = "bundle" # Add randomness to ensure uniqueness - random_suffix = os.urandom( - 8 - ).hex() # Increased from 4 to 8 bytes for more entropy + random_suffix = os.urandom(8).hex() # Increased from 4 to 8 bytes for more entropy return f"{sanitized}_{random_suffix}" @@ -1720,9 +1600,7 @@ async def check_api_server_available(self) -> bool: # Try to find kubeconfig in current directory (where sbctl might create it) current_dir_kubeconfig = Path.cwd() / "kubeconfig" if current_dir_kubeconfig.exists(): - logger.info( - f"Found kubeconfig in current directory: {current_dir_kubeconfig}" - ) + logger.info(f"Found kubeconfig in current directory: {current_dir_kubeconfig}") kubeconfig_path = current_dir_kubeconfig # Try to parse kubeconfig if found @@ -1732,9 +1610,7 @@ async def check_api_server_available(self) -> bool: with open(kubeconfig_path, "r") as f: kubeconfig_content = f.read() - logger.debug( - f"Kubeconfig content (first 200 chars): {kubeconfig_content[:200]}..." - ) + logger.debug(f"Kubeconfig content (first 200 chars): {kubeconfig_content[:200]}...") # Try parsing as JSON first, then try YAML or manual parsing as fallback config = {} @@ -1762,12 +1638,8 @@ async def check_api_server_available(self) -> bool: ) if server_matches: server_url = server_matches[0].strip() - config = { - "clusters": [{"cluster": {"server": server_url}}] - } - logger.debug( - f"Extracted server URL using regex: {server_url}" - ) + config = {"clusters": [{"cluster": {"server": server_url}}]} + logger.debug(f"Extracted server URL using regex: {server_url}") else: logger.warning( "Could not extract server URL from kubeconfig with regex" @@ -1807,9 +1679,7 @@ async def check_api_server_available(self) -> bool: port = int(server_url.split(":")[-1]) logger.debug(f"Extracted API server port directly: {port}") except (ValueError, IndexError) as e: - logger.warning( - f"Failed to extract port from server URL: {e}" - ) + logger.warning(f"Failed to extract port from server URL: {e}") except (json.JSONDecodeError, KeyError, ValueError, IndexError) as e: logger.warning(f"Failed to parse kubeconfig: {e}") @@ -1829,13 +1699,9 @@ async def check_api_server_available(self) -> bool: from .subprocess_utils import pipe_transport_reader try: - async with pipe_transport_reader( - self.sbctl_process.stdout - ) as stdout_reader: + async with pipe_transport_reader(self.sbctl_process.stdout) as stdout_reader: # Set a timeout for reading - data = await asyncio.wait_for( - stdout_reader.read(1024), timeout=0.5 - ) + data = await asyncio.wait_for(stdout_reader.read(1024), timeout=0.5) if data: output = data.decode("utf-8", errors="replace") logger.debug(f"sbctl process output: {output}") @@ -1855,9 +1721,7 @@ async def check_api_server_available(self) -> bool: parsed_url = urlparse(url) if parsed_url.port: port = parsed_url.port - logger.debug( - f"Using port from sbctl output: {port}" - ) + logger.debug(f"Using port from sbctl output: {port}") if parsed_url.hostname: host = parsed_url.hostname except Exception: @@ -1893,9 +1757,7 @@ async def check_api_server_available(self) -> bool: # Get response body for debugging try: - body = await asyncio.wait_for( - response.text(), timeout=1.0 - ) + body = await asyncio.wait_for(response.text(), timeout=1.0) logger.debug( f"Response from {url} (first 200 chars): {body[:200]}..." ) @@ -1919,9 +1781,7 @@ async def check_api_server_available(self) -> bool: async with aiohttp.ClientSession(timeout=backup_timeout) as session: for endpoint in endpoints: url = f"http://{host}:{port}{endpoint}" - logger.debug( - f"Checking API server with backup aiohttp check: {url}" - ) + logger.debug(f"Checking API server with backup aiohttp check: {url}") try: async with session.get(url) as response: @@ -1977,8 +1837,7 @@ async def get_diagnostic_info(self) -> dict[str, object]: "sbctl_process_running": self.sbctl_process is not None and self.sbctl_process.returncode is None, "api_server_available": await self.check_api_server_available(), - "bundle_initialized": self.active_bundle is not None - and self.active_bundle.initialized, + "bundle_initialized": self.active_bundle is not None and self.active_bundle.initialized, "system_info": await self._get_system_info(), } @@ -2083,9 +1942,7 @@ async def _get_system_info(self) -> dict[str, object]: except Exception as e: info["socket_port_check_exception"] = str(e) - logger.warning( - f"Error during Python socket port check for port {port}: {e}" - ) + logger.warning(f"Error during Python socket port check for port {port}: {e}") # Fallback: assume port is not listening if we can't check info[f"port_{port}_listening"] = False info[f"port_{port}_details"] = f"Could not check port {port}: {e}" @@ -2102,9 +1959,7 @@ async def _get_system_info(self) -> dict[str, object]: try: body = await asyncio.wait_for(response.text(), timeout=1.0) if body: - info[f"http_{port}_response_body"] = body[ - :200 - ] # Limit body size + info[f"http_{port}_response_body"] = body[:200] # Limit body size except (asyncio.TimeoutError, UnicodeDecodeError): pass except (aiohttp.ClientError, asyncio.TimeoutError) as e: @@ -2117,9 +1972,7 @@ async def _get_system_info(self) -> dict[str, object]: return info - async def list_available_bundles( - self, include_invalid: bool = False - ) -> List[BundleFileInfo]: + async def list_available_bundles(self, include_invalid: bool = False) -> List[BundleFileInfo]: """ List available support bundles in the bundle storage directory. @@ -2162,16 +2015,12 @@ async def list_available_bundles( try: valid, validation_message = self._check_bundle_validity(file_path) except Exception as e: - logger.warning( - f"Error checking bundle validity for {file_path}: {str(e)}" - ) + logger.warning(f"Error checking bundle validity for {file_path}: {str(e)}") validation_message = f"Error checking validity: {str(e)}" # Skip invalid bundles if requested if not valid and not include_invalid: - logger.debug( - f"Skipping invalid bundle {file_path}: {validation_message}" - ) + logger.debug(f"Skipping invalid bundle {file_path}: {validation_message}") continue # Create the bundle info @@ -2199,15 +2048,9 @@ async def list_available_bundles( path=str(file_path), relative_path=file_path.name, name=file_path.name, - size_bytes=( - file_path.stat().st_size - if file_path.exists() - else 0 - ), + size_bytes=(file_path.stat().st_size if file_path.exists() else 0), modified_time=( - file_path.stat().st_mtime - if file_path.exists() - else 0 + file_path.stat().st_mtime if file_path.exists() else 0 ), valid=False, validation_message=f"Error: {str(e)}", @@ -2256,9 +2099,7 @@ def _check_bundle_validity(self, file_path: Path) -> Tuple[bool, Optional[str]]: try: with tarfile.open(file_path, "r:gz") as tar: # List first few entries to check structure without extracting - members = tar.getmembers()[ - :20 - ] # Just check the first 20 entries for efficiency + members = tar.getmembers()[:20] # Just check the first 20 entries for efficiency # Look for patterns that indicate a support bundle has_cluster_resources = False @@ -2337,9 +2178,7 @@ async def cleanup(self) -> None: try: proc.terminate() terminated_count += 1 - logger.debug( - f"Terminated sbctl process with PID {proc.pid}" - ) + logger.debug(f"Terminated sbctl process with PID {proc.pid}") except (psutil.NoSuchProcess, psutil.AccessDenied): # Process already gone or access denied - skip it continue @@ -2360,9 +2199,7 @@ async def cleanup(self) -> None: try: logger.info(f"Removing temporary bundle directory: {self.bundle_dir}") shutil.rmtree(self.bundle_dir) - logger.info( - f"Successfully removed temporary bundle directory: {self.bundle_dir}" - ) + logger.info(f"Successfully removed temporary bundle directory: {self.bundle_dir}") except Exception as e: logger.error(f"Failed to remove temporary bundle directory: {str(e)}") diff --git a/src/mcp_server_troubleshoot/cli.py b/src/mcp_server_troubleshoot/cli.py index 3dbb54c..30d4c07 100644 --- a/src/mcp_server_troubleshoot/cli.py +++ b/src/mcp_server_troubleshoot/cli.py @@ -58,9 +58,7 @@ def setup_logging(verbose: bool = False, mcp_mode: bool = False) -> None: def parse_args() -> argparse.Namespace: """Parse command-line arguments for the MCP server.""" - parser = argparse.ArgumentParser( - description="MCP server for Kubernetes support bundles" - ) + parser = argparse.ArgumentParser(description="MCP server for Kubernetes support bundles") parser.add_argument("--bundle-dir", type=Path, help="Directory to store bundles") parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") parser.add_argument( diff --git a/src/mcp_server_troubleshoot/files.py b/src/mcp_server_troubleshoot/files.py index 54dccf7..1288273 100644 --- a/src/mcp_server_troubleshoot/files.py +++ b/src/mcp_server_troubleshoot/files.py @@ -101,9 +101,7 @@ class ReadFileArgs(BaseModel): """ path: str = Field(description="The path to the file within the bundle") - start_line: int = Field( - 0, description="The line number to start reading from (0-indexed)" - ) + start_line: int = Field(0, description="The line number to start reading from (0-indexed)") end_line: Optional[int] = Field( None, description="The line number to end reading at (0-indexed, inclusive)" ) @@ -157,16 +155,10 @@ class GrepFilesArgs(BaseModel): pattern: str = Field(description="The pattern to search for") path: str = Field(description="The path within the bundle to search") recursive: bool = Field(True, description="Whether to search recursively") - glob_pattern: Optional[str] = Field( - None, description="The glob pattern to match files against" - ) - case_sensitive: bool = Field( - False, description="Whether the search is case-sensitive" - ) + glob_pattern: Optional[str] = Field(None, description="The glob pattern to match files against") + case_sensitive: bool = Field(False, description="Whether the search is case-sensitive") max_results: int = Field(1000, description="Maximum number of results to return") - max_results_per_file: int = Field( - 5, description="Maximum number of results to return per file" - ) + max_results_per_file: int = Field(5, description="Maximum number of results to return per file") max_files: int = Field(10, description="Maximum number of files to search/return") verbosity: Optional[str] = Field( None, @@ -230,20 +222,14 @@ class FileInfo(BaseModel): """ name: str = Field(description="The name of the file or directory") - path: str = Field( - description="The path of the file or directory relative to the bundle root" - ) + path: str = Field(description="The path of the file or directory relative to the bundle root") type: str = Field(description="The type of the entry ('file' or 'dir')") size: int = Field(description="The size of the file in bytes (0 for directories)") - access_time: float = Field( - description="The time of most recent access (seconds since epoch)" - ) + access_time: float = Field(description="The time of most recent access (seconds since epoch)") modify_time: float = Field( description="The time of most recent content modification (seconds since epoch)" ) - is_binary: bool = Field( - description="Whether the file appears to be binary (for files)" - ) + is_binary: bool = Field(description="Whether the file appears to be binary (for files)") class FileListResult(BaseModel): @@ -265,9 +251,7 @@ class FileContentResult(BaseModel): path: str = Field(description="The path of the file that was read") content: str = Field(description="The content of the file") - start_line: int = Field( - description="The line number that was started from (0-indexed)" - ) + start_line: int = Field(description="The line number that was started from (0-indexed)") end_line: int = Field(description="The line number that was ended at (0-indexed)") total_lines: int = Field(description="The total number of lines in the file") binary: bool = Field(description="Whether the file appears to be binary") @@ -292,16 +276,12 @@ class GrepResult(BaseModel): pattern: str = Field(description="The pattern that was searched for") path: str = Field(description="The path that was searched") - glob_pattern: Optional[str] = Field( - description="The glob pattern that was used, if any" - ) + glob_pattern: Optional[str] = Field(description="The glob pattern that was used, if any") matches: List[GrepMatch] = Field(description="The matches found") total_matches: int = Field(description="The total number of matches found") files_searched: int = Field(description="The number of files that were searched") case_sensitive: bool = Field(description="Whether the search was case-sensitive") - truncated: bool = Field( - description="Whether the results were truncated due to max_results" - ) + truncated: bool = Field(description="Whether the results were truncated due to max_results") files_truncated: bool = Field( default=False, description="Whether the file list was truncated due to max_files", @@ -352,9 +332,7 @@ def _get_bundle_path(self) -> Path: if support_bundle_dirs: support_bundle_dir = support_bundle_dirs[0] # Use the first one found if support_bundle_dir.exists() and support_bundle_dir.is_dir(): - logger.debug( - f"Using extracted bundle subdirectory: {support_bundle_dir}" - ) + logger.debug(f"Using extracted bundle subdirectory: {support_bundle_dir}") return support_bundle_dir # If no support-bundle-* directory, check if the extracted directory itself has files @@ -573,9 +551,7 @@ async def read_file( # For binary files, just read the whole file if is_binary: if start_line > 0 or end_line is not None: - logger.warning( - "Line range filtering not supported for binary files" - ) + logger.warning("Line range filtering not supported for binary files") content = f.read() if isinstance(content, bytes): # For binary, return a hex dump diff --git a/src/mcp_server_troubleshoot/formatters.py b/src/mcp_server_troubleshoot/formatters.py index f55ce3d..2bfc779 100644 --- a/src/mcp_server_troubleshoot/formatters.py +++ b/src/mcp_server_troubleshoot/formatters.py @@ -67,9 +67,7 @@ def format_bundle_initialization( if api_server_available: return json.dumps({"bundle_id": metadata.id, "status": "ready"}) else: - return json.dumps( - {"bundle_id": metadata.id, "status": "api_unavailable"} - ) + return json.dumps({"bundle_id": metadata.id, "status": "api_unavailable"}) elif self.verbosity == VerbosityLevel.STANDARD: result = { @@ -134,9 +132,9 @@ def format_bundle_list(self, bundles: List[BundleFileInfo]) -> str: # Format modification time import datetime - modified_time_str = datetime.datetime.fromtimestamp( - bundle.modified_time - ).strftime("%Y-%m-%d %H:%M:%S") + modified_time_str = datetime.datetime.fromtimestamp(bundle.modified_time).strftime( + "%Y-%m-%d %H:%M:%S" + ) bundle_entry = { "name": bundle.name, @@ -155,21 +153,15 @@ def format_bundle_list(self, bundles: List[BundleFileInfo]) -> str: bundle_list.append(bundle_entry) response_obj = {"bundles": bundle_list, "total": len(bundle_list)} - response = ( - f"```json\n{json.dumps(response_obj, separators=(',', ':'))}\n```\n\n" - ) + response = f"```json\n{json.dumps(response_obj, separators=(',', ':'))}\n```\n\n" # Add usage instructions - example_bundle = next( - (b for b in bundles if b.valid), bundles[0] if bundles else None - ) + example_bundle = next((b for b in bundles if b.valid), bundles[0] if bundles else None) if example_bundle: response += "## Usage Instructions\n\n" response += "To use one of these bundles, initialize it with the `initialize_bundle` tool using the `source` value:\n\n" response += ( - '```json\n{\n "source": "' - + example_bundle.relative_path - + '"\n}\n```\n\n' + '```json\n{\n "source": "' + example_bundle.relative_path + '"\n}\n```\n\n' ) response += "After initializing a bundle, you can explore its contents using the file exploration tools (`list_files`, `read_file`, `grep_files`) and run kubectl commands with the `kubectl` tool." @@ -180,10 +172,7 @@ def format_file_list(self, result: FileListResult) -> str: if self.verbosity == VerbosityLevel.MINIMAL: return json.dumps( - [ - entry.name + ("/" if entry.type == "dir" else "") - for entry in result.entries - ] + [entry.name + ("/" if entry.type == "dir" else "") for entry in result.entries] ) elif self.verbosity == VerbosityLevel.STANDARD: @@ -247,9 +236,7 @@ def format_file_content(self, result: FileContentResult) -> str: response = f"Read {file_type} file {result.path} (lines {result.start_line + 1}-{result.end_line + 1} of {result.total_lines}):\n" response += f"```\n{content_with_numbers}```" else: - response = ( - f"Read {file_type} file {result.path} (binary data shown as hex):\n" - ) + response = f"Read {file_type} file {result.path} (binary data shown as hex):\n" response += f"```\n{result.content}\n```" return response @@ -312,9 +299,7 @@ def format_grep_results(self, result: GrepResult) -> str: else: # VERBOSE or DEBUG # Current full format - pattern_type = ( - "case-sensitive" if result.case_sensitive else "case-insensitive" - ) + pattern_type = "case-sensitive" if result.case_sensitive else "case-insensitive" path_desc = result.path + ( f" (matching {result.glob_pattern})" if result.glob_pattern else "" ) @@ -366,13 +351,9 @@ def format_kubectl_result(self, result: KubectlResult) -> str: elif self.verbosity == VerbosityLevel.STANDARD: if result.is_json: - return json.dumps( - {"output": result.output, "exit_code": result.exit_code} - ) + return json.dumps({"output": result.output, "exit_code": result.exit_code}) else: - return json.dumps( - {"output": result.stdout, "exit_code": result.exit_code} - ) + return json.dumps({"output": result.stdout, "exit_code": result.exit_code}) else: # VERBOSE or DEBUG # Current full format @@ -381,9 +362,7 @@ def format_kubectl_result(self, result: KubectlResult) -> str: response = f"kubectl command executed successfully:\n```json\n{output_str}\n```" else: output_str = result.stdout - response = ( - f"kubectl command executed successfully:\n```\n{output_str}\n```" - ) + response = f"kubectl command executed successfully:\n```\n{output_str}\n```" metadata = { "command": result.command, @@ -399,9 +378,7 @@ def format_kubectl_result(self, result: KubectlResult) -> str: return response - def format_error( - self, error_message: str, diagnostics: Optional[Dict[str, Any]] = None - ) -> str: + def format_error(self, error_message: str, diagnostics: Optional[Dict[str, Any]] = None) -> str: """Format error messages based on verbosity level.""" if self.verbosity == VerbosityLevel.MINIMAL: @@ -417,9 +394,7 @@ def format_error( response += f"\n\nDiagnostic information:\n```json\n{json.dumps(diagnostics, separators=(',', ':'))}\n```" return response - def format_overflow_message( - self, tool_name: str, estimated_tokens: int, content: str - ) -> str: + def format_overflow_message(self, tool_name: str, estimated_tokens: int, content: str) -> str: """ Format helpful overflow message with tool-specific guidance. @@ -477,12 +452,8 @@ def _get_tool_specific_suggestions(self, tool_name: str) -> List[str]: "Target specific namespaces: kubectl get pods -n specific-namespace", "Limit results: kubectl get pods --limit=50", ], - "initialize_bundle": [ - 'Use minimal verbosity: initialize_bundle(verbosity="minimal")' - ], - "list_bundles": [ - 'Use minimal verbosity: list_bundles(verbosity="minimal")' - ], + "initialize_bundle": ['Use minimal verbosity: initialize_bundle(verbosity="minimal")'], + "list_bundles": ['Use minimal verbosity: list_bundles(verbosity="minimal")'], } return suggestions_map.get( @@ -523,16 +494,16 @@ def _generate_content_preview(self, content: str) -> str: lines = content.split("\n") total_lines = len(lines) - preview_msg = f"Showing first {preview_chars} characters (content has {total_lines:,} lines):\n" + preview_msg = ( + f"Showing first {preview_chars} characters (content has {total_lines:,} lines):\n" + ) preview_msg += "```\n" + preview_text + "\n```\n" # Add helpful context about the content structure if content.strip().startswith("{") or content.strip().startswith("["): preview_msg += "\n*Note: Content appears to be JSON format*\n" elif "```" in content: - preview_msg += ( - "\n*Note: Content contains code blocks or formatted sections*\n" - ) + preview_msg += "\n*Note: Content contains code blocks or formatted sections*\n" return preview_msg diff --git a/src/mcp_server_troubleshoot/kubectl.py b/src/mcp_server_troubleshoot/kubectl.py index 142ae9e..39d252b 100644 --- a/src/mcp_server_troubleshoot/kubectl.py +++ b/src/mcp_server_troubleshoot/kubectl.py @@ -82,9 +82,7 @@ def validate_command(cls, v: str) -> str: ] for op in dangerous_operations: if re.search(rf"^\s*{op}\b", v): - raise ValueError( - f"Kubectl command '{op}' is not allowed for safety reasons" - ) + raise ValueError(f"Kubectl command '{op}' is not allowed for safety reasons") return v @@ -100,9 +98,7 @@ class KubectlResult(BaseModel): stderr: str = Field(description="The standard error output of the command") output: Any = Field(description="The parsed output, if applicable") is_json: bool = Field(description="Whether the output is JSON") - duration_ms: int = Field( - description="The duration of the command execution in milliseconds" - ) + duration_ms: int = Field(description="The duration of the command execution in milliseconds") @field_validator("exit_code") @classmethod @@ -166,9 +162,7 @@ async def execute( ) # Construct the command - return await self._run_kubectl_command( - command, active_bundle, timeout, json_output - ) + return await self._run_kubectl_command(command, active_bundle, timeout, json_output) async def _run_kubectl_command( self, command: str, bundle: BundleMetadata, timeout: int, json_output: bool @@ -232,9 +226,7 @@ async def _run_kubectl_command( stderr_str = stderr.decode("utf-8") # Process the output - output, is_json = self._process_output( - stdout_str, returncode == 0 and json_output - ) + output, is_json = self._process_output(stdout_str, returncode == 0 and json_output) # Create the result result = KubectlResult( @@ -249,22 +241,16 @@ async def _run_kubectl_command( # Log the result if returncode == 0: - logger.info( - f"kubectl command completed successfully in {duration_ms}ms" - ) + logger.info(f"kubectl command completed successfully in {duration_ms}ms") else: - logger.error( - f"kubectl command failed with exit code {returncode}: {stderr_str}" - ) + logger.error(f"kubectl command failed with exit code {returncode}: {stderr_str}") raise KubectlError("kubectl command failed", returncode, stderr_str) return result except (OSError, FileNotFoundError) as e: logger.exception(f"Error executing kubectl command: {str(e)}") - raise KubectlError( - "Failed to execute kubectl command", 1, f"Error: {str(e)}" - ) + raise KubectlError("Failed to execute kubectl command", 1, f"Error: {str(e)}") def _process_output(self, output: str, try_json: bool) -> Tuple[Any, bool]: """ diff --git a/src/mcp_server_troubleshoot/lifecycle.py b/src/mcp_server_troubleshoot/lifecycle.py index 11efb68..181ac06 100644 --- a/src/mcp_server_troubleshoot/lifecycle.py +++ b/src/mcp_server_troubleshoot/lifecycle.py @@ -56,9 +56,7 @@ def create_temp_directory() -> str: return temp_dir -async def periodic_bundle_cleanup( - bundle_manager: BundleManager, interval: int = 3600 -) -> None: +async def periodic_bundle_cleanup(bundle_manager: BundleManager, interval: int = 3600) -> None: """Periodically clean up old bundles.""" logger.info(f"Starting periodic bundle cleanup (interval: {interval}s)") try: @@ -94,9 +92,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: bundle_dir_str = os.environ.get("MCP_BUNDLE_STORAGE") bundle_dir = Path(bundle_dir_str) if bundle_dir_str else None - enable_periodic_cleanup = os.environ.get( - "ENABLE_PERIODIC_CLEANUP", "false" - ).lower() in ( + enable_periodic_cleanup = os.environ.get("ENABLE_PERIODIC_CLEANUP", "false").lower() in ( "true", "1", "yes", @@ -120,9 +116,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # Start periodic cleanup task if configured if enable_periodic_cleanup: - logger.info( - f"Enabling periodic bundle cleanup every {cleanup_interval} seconds" - ) + logger.info(f"Enabling periodic bundle cleanup every {cleanup_interval} seconds") background_tasks["bundle_cleanup"] = asyncio.create_task( periodic_bundle_cleanup(bundle_manager, cleanup_interval) ) @@ -163,9 +157,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: try: await asyncio.wait_for(asyncio.shield(task), timeout=5.0) except (asyncio.CancelledError, asyncio.TimeoutError): - logger.warning( - f"Task {name} did not complete gracefully within timeout" - ) + logger.warning(f"Task {name} did not complete gracefully within timeout") # Clean up bundle manager resources try: diff --git a/src/mcp_server_troubleshoot/server.py b/src/mcp_server_troubleshoot/server.py index e62356b..7af7398 100644 --- a/src/mcp_server_troubleshoot/server.py +++ b/src/mcp_server_troubleshoot/server.py @@ -183,9 +183,7 @@ async def initialize_bundle(args: InitializeBundleArgs) -> List[TextContent]: diagnostics = await bundle_manager.get_diagnostic_info() # Format response using the formatter - response = formatter.format_bundle_initialization( - result, api_server_available, diagnostics - ) + response = formatter.format_bundle_initialization(result, api_server_available, diagnostics) return check_response_size(response, "initialize_bundle", formatter) except BundleManagerError as e: @@ -316,9 +314,7 @@ async def kubectl(args: KubectlCommandArgs) -> List[TextContent]: return check_response_size(formatted_error, "kubectl", formatter) # Execute the kubectl command - result = await get_kubectl_executor().execute( - args.command, args.timeout, args.json_output - ) + result = await get_kubectl_executor().execute(args.command, args.timeout, args.json_output) # Format response using the formatter response = formatter.format_kubectl_result(result) @@ -334,10 +330,7 @@ async def kubectl(args: KubectlCommandArgs) -> List[TextContent]: diagnostics = await bundle_manager.get_diagnostic_info() # Check if this is a connection issue - if ( - "connection refused" in str(e).lower() - or "could not connect" in str(e).lower() - ): + if "connection refused" in str(e).lower() or "could not connect" in str(e).lower(): error_message += ( " This appears to be a connection issue with the Kubernetes API server. " "The API server may not be running properly. " @@ -450,9 +443,7 @@ async def read_file(args: ReadFileArgs) -> List[TextContent]: formatter = get_formatter(args.verbosity) try: - result = await get_file_explorer().read_file( - args.path, args.start_line, args.end_line - ) + result = await get_file_explorer().read_file(args.path, args.start_line, args.end_line) response = formatter.format_file_content(result) return check_response_size(response, "read_file", formatter) diff --git a/src/mcp_server_troubleshoot/subprocess_utils.py b/src/mcp_server_troubleshoot/subprocess_utils.py index 9e7f189..48bd972 100644 --- a/src/mcp_server_troubleshoot/subprocess_utils.py +++ b/src/mcp_server_troubleshoot/subprocess_utils.py @@ -50,9 +50,7 @@ def _safe_transport_cleanup(transport: Any) -> None: is_closing = transport.is_closing() logger.debug(f"Transport is_closing status: {is_closing}") else: - logger.debug( - "Transport doesn't have is_closing method, assuming closed" - ) + logger.debug("Transport doesn't have is_closing method, assuming closed") except AttributeError as e: # This is the specific error we're trying to avoid logger.debug( @@ -151,9 +149,7 @@ async def pipe_transport_reader( _safe_transport_cleanup(transport) # Wait for transport to close safely - await _safe_transport_wait_close( - transport, timeout_per_check=0.1, max_checks=10 - ) + await _safe_transport_wait_close(transport, timeout_per_check=0.1, max_checks=10) async def subprocess_exec_with_cleanup( @@ -195,9 +191,7 @@ async def subprocess_exec_with_cleanup( process = await asyncio.create_subprocess_exec(*args, **subprocess_kwargs) if timeout is not None: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=timeout - ) + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) else: stdout, stderr = await process.communicate() @@ -276,9 +270,7 @@ async def subprocess_shell_with_cleanup( process = await asyncio.create_subprocess_shell(command, **subprocess_kwargs) if timeout is not None: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=timeout - ) + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) else: stdout, stderr = await process.communicate() diff --git a/test_bundle_loading.py b/test_bundle_loading.py index 716f49e..0d57f9d 100644 --- a/test_bundle_loading.py +++ b/test_bundle_loading.py @@ -49,9 +49,7 @@ async def test_bundle_loading(): await client.initialize_mcp() print("✅ MCP initialized") - print( - "\n=== Step 3: Testing initialize_bundle tool (with timeout tracking) ===" - ) + print("\n=== Step 3: Testing initialize_bundle tool (with timeout tracking) ===") print(f"Calling initialize_bundle with path: {test_bundle_copy}") # Add timeout tracking to see where it gets stuck @@ -62,9 +60,7 @@ async def test_bundle_loading(): try: print("Sending tool call request...") content = await asyncio.wait_for( - client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ), + client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}), timeout=30.0, # 30 second timeout to see what happens ) elapsed = time.time() - start_time diff --git a/test_initialize_bundle_tool.py b/test_initialize_bundle_tool.py index 8b8bbb1..f11dfb8 100644 --- a/test_initialize_bundle_tool.py +++ b/test_initialize_bundle_tool.py @@ -75,9 +75,7 @@ async def test_initialize_bundle_tool(): # Read initialize response if process.stdout: - init_response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=5.0 - ) + init_response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) init_response = init_response_bytes.decode().strip() print(f"Initialize response: {init_response}") @@ -103,9 +101,7 @@ async def test_initialize_bundle_tool(): # Try to read response with timeout try: if process.stdout: - print( - "Waiting for tool response (this should complete in ~6 seconds)..." - ) + print("Waiting for tool response (this should complete in ~6 seconds)...") response_bytes = await asyncio.wait_for( process.stdout.readline(), timeout=30.0, # Generous timeout @@ -130,9 +126,7 @@ async def test_initialize_bundle_tool(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for( - process.stderr.read(4096), timeout=1.0 - ) + stderr_data = await asyncio.wait_for(process.stderr.read(4096), timeout=1.0) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR during timeout: {stderr_text}") diff --git a/test_mcp_communication.py b/test_mcp_communication.py index 6d1067f..7ef84bd 100644 --- a/test_mcp_communication.py +++ b/test_mcp_communication.py @@ -15,9 +15,7 @@ sys.path.insert(0, str(Path(__file__).parent)) # Set up minimal logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") async def test_mcp_communication(): @@ -67,9 +65,7 @@ async def test_mcp_communication(): # Try to read response with timeout try: if process.stdout: - response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=5.0 - ) + response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) response_line = response_bytes.decode().strip() print(f"Response: {response_line}") @@ -89,9 +85,7 @@ async def test_mcp_communication(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for( - process.stderr.read(4096), timeout=1.0 - ) + stderr_data = await asyncio.wait_for(process.stderr.read(4096), timeout=1.0) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR: {stderr_text}") diff --git a/test_minimal_mcp.py b/test_minimal_mcp.py index 551cdd2..c0ed104 100644 --- a/test_minimal_mcp.py +++ b/test_minimal_mcp.py @@ -83,9 +83,7 @@ async def hello() -> list[TextContent]: # Try to get response try: if process.stdout: - response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=5.0 - ) + response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=5.0) response_line = response_bytes.decode().strip() print(f"Response: {response_line}") diff --git a/test_module_startup.py b/test_module_startup.py index 9553f5e..3479129 100644 --- a/test_module_startup.py +++ b/test_module_startup.py @@ -71,9 +71,7 @@ async def test_module_startup(): try: if process.stdout: print("Waiting for response...") - response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=10.0 - ) + response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=10.0) response_line = response_bytes.decode().strip() print(f"✅ Response: {response_line}") @@ -90,9 +88,7 @@ async def test_module_startup(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for( - process.stderr.read(2048), timeout=1.0 - ) + stderr_data = await asyncio.wait_for(process.stderr.read(2048), timeout=1.0) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR during timeout: {stderr_text}") diff --git a/test_production_server.py b/test_production_server.py index 88fe636..cb4f4a6 100644 --- a/test_production_server.py +++ b/test_production_server.py @@ -71,9 +71,7 @@ async def test_production_server(): try: if process.stdout: print("Waiting for response...") - response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=10.0 - ) + response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=10.0) response_line = response_bytes.decode().strip() print(f"✅ Response: {response_line}") @@ -96,9 +94,7 @@ async def test_production_server(): # Check stderr for errors if process.stderr: try: - stderr_data = await asyncio.wait_for( - process.stderr.read(4096), timeout=1.0 - ) + stderr_data = await asyncio.wait_for(process.stderr.read(4096), timeout=1.0) if stderr_data: stderr_text = stderr_data.decode() print(f"STDERR during timeout: {stderr_text}") diff --git a/test_sbctl_direct.py b/test_sbctl_direct.py index 306186d..962ff45 100644 --- a/test_sbctl_direct.py +++ b/test_sbctl_direct.py @@ -80,13 +80,9 @@ async def test_sbctl_direct(): print(f" Found kubeconfig files: {kubeconfig_files}") # Look for local-kubeconfig files - local_kubeconfig_files = list( - location.glob("**/local-kubeconfig*") - ) + local_kubeconfig_files = list(location.glob("**/local-kubeconfig*")) if local_kubeconfig_files: - print( - f" Found local-kubeconfig files: {local_kubeconfig_files}" - ) + print(f" Found local-kubeconfig files: {local_kubeconfig_files}") except Exception as search_e: print(f" Error searching {location}: {search_e}") diff --git a/test_simple_mcp.py b/test_simple_mcp.py index f9fee80..68c165d 100644 --- a/test_simple_mcp.py +++ b/test_simple_mcp.py @@ -54,9 +54,7 @@ async def test_simple_mcp(): # Wait for any response at all try: if process.stdout: - response_bytes = await asyncio.wait_for( - process.stdout.readline(), timeout=3.0 - ) + response_bytes = await asyncio.wait_for(process.stdout.readline(), timeout=3.0) response_line = response_bytes.decode().strip() print(f"Got response: {response_line}") @@ -78,9 +76,7 @@ async def test_simple_mcp(): # Read stderr to see what's happening if process.stderr: try: - stderr_data = await asyncio.wait_for( - process.stderr.read(1024), timeout=1.0 - ) + stderr_data = await asyncio.wait_for(process.stderr.read(1024), timeout=1.0) if stderr_data: print(f"STDERR: {stderr_data.decode()}") except asyncio.TimeoutError: diff --git a/tests/conftest.py b/tests/conftest.py index 761eb31..8257dd3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -88,9 +88,7 @@ def clean_asyncio(): # Only log specific understood errors to avoid silent failures import logging - logging.getLogger("tests").debug( - f"Controlled exception during event loop cleanup: {e}" - ) + logging.getLogger("tests").debug(f"Controlled exception during event loop cleanup: {e}") # Create a new event loop for the next test asyncio.set_event_loop(asyncio.new_event_loop()) @@ -227,9 +225,7 @@ def container_image(request): # Skip container builds in CI due to melange/apko limitations # Container builds are validated in the publish workflow if os.environ.get("CI") == "true": - pytest.skip( - "Container image builds are skipped in CI - run locally with 'pytest -m slow'" - ) + pytest.skip("Container image builds are skipped in CI - run locally with 'pytest -m slow'") # Get project root directory project_root = Path(__file__).parents[1] @@ -268,9 +264,7 @@ def container_image(request): timeout=10, check=False, ) - containers = ( - containers_result.stdout.strip().split("\n") if containers_result.stdout else [] - ) + containers = containers_result.stdout.strip().split("\n") if containers_result.stdout else [] for container_id in containers: if container_id: diff --git a/tests/e2e/test_build_reliability.py b/tests/e2e/test_build_reliability.py index 256d4c7..ad3a2df 100644 --- a/tests/e2e/test_build_reliability.py +++ b/tests/e2e/test_build_reliability.py @@ -101,9 +101,7 @@ def test_container_build_never_uses_cached_configs(temp_project_dir): finally: # Clean up any test images - subprocess.run( - [runtime, "rmi", "-f", f"{image_name}:latest"], capture_output=True - ) + subprocess.run([runtime, "rmi", "-f", f"{image_name}:latest"], capture_output=True) def test_build_config_changes_reflected_in_tests(): @@ -120,19 +118,19 @@ def test_build_config_changes_reflected_in_tests(): conftest_content = conftest_path.read_text() # Verify the problematic caching code was removed - assert ( - "Using existing container image for tests" not in conftest_content - ), "The problematic caching logic should have been removed from conftest.py" + assert "Using existing container image for tests" not in conftest_content, ( + "The problematic caching logic should have been removed from conftest.py" + ) # Verify the fix is in place - assert ( - "Building container image (Podman will use layer cache" in conftest_content - ), "The fix to always build container images should be present in conftest.py" + assert "Building container image (Podman will use layer cache" in conftest_content, ( + "The fix to always build container images should be present in conftest.py" + ) # Verify we always build (no dangerous skip logic) - assert ( - "Always run the build process" in conftest_content - ), "Tests should always run build process and rely on Podman layer caching" + assert "Always run the build process" in conftest_content, ( + "Tests should always run build process and rely on Podman layer caching" + ) def test_sbctl_installation_path_is_correct(): @@ -149,14 +147,14 @@ def test_sbctl_installation_path_is_correct(): config_content = melange_config.read_text() # Verify the fix is in place - assert ( - "${{targets.destdir}}/usr/bin" in config_content - ), "sbctl should be installed to ${{targets.destdir}}/usr/bin for proper packaging" + assert "${{targets.destdir}}/usr/bin" in config_content, ( + "sbctl should be installed to ${{targets.destdir}}/usr/bin for proper packaging" + ) # Verify the broken pattern is not present - assert ( - "/usr/local/bin/sbctl" not in config_content - ), "sbctl should not be installed to /usr/local/bin (the broken path)" + assert "/usr/local/bin/sbctl" not in config_content, ( + "sbctl should not be installed to /usr/local/bin (the broken path)" + ) # Verify the installation command sequence is correct lines = config_content.split("\n") diff --git a/tests/e2e/test_container_bundle_validation.py b/tests/e2e/test_container_bundle_validation.py index caddd11..d72cf88 100644 --- a/tests/e2e/test_container_bundle_validation.py +++ b/tests/e2e/test_container_bundle_validation.py @@ -95,9 +95,7 @@ async def send_request(self, method: str, params: Optional[dict] = None) -> dict raise RuntimeError("Container stdout not available") try: - response_bytes = await asyncio.wait_for( - self.process.stdout.readline(), timeout=60.0 - ) + response_bytes = await asyncio.wait_for(self.process.stdout.readline(), timeout=60.0) response_line = response_bytes.decode().strip() logger.debug(f"Received: {response_line}") @@ -123,9 +121,7 @@ async def send_request(self, method: str, params: Optional[dict] = None) -> dict async def call_tool(self, tool_name: str, arguments: dict) -> dict: """Call an MCP tool in the container.""" - return await self.send_request( - "tools/call", {"name": tool_name, "arguments": arguments} - ) + return await self.send_request("tools/call", {"name": tool_name, "arguments": arguments}) async def stop(self) -> None: """Stop the container.""" @@ -344,17 +340,13 @@ async def test_container_complete_workflow( # Step 1: Initialize bundle bundle_path = f"/data/bundles/{test_bundle_in_dir.name}" - init_response = await client.call_tool( - "initialize_bundle", {"source": bundle_path} - ) + init_response = await client.call_tool("initialize_bundle", {"source": bundle_path}) assert "result" in init_response logger.info("✅ Bundle initialized in container") # Step 2: List files - files_response = await client.call_tool( - "list_files", {"path": "/", "recursive": False} - ) + files_response = await client.call_tool("list_files", {"path": "/", "recursive": False}) assert "result" in files_response files_content = files_response["result"]["content"][0]["text"] diff --git a/tests/e2e/test_container_production_validation.py b/tests/e2e/test_container_production_validation.py index b840337..b9ddd66 100644 --- a/tests/e2e/test_container_production_validation.py +++ b/tests/e2e/test_container_production_validation.py @@ -63,9 +63,9 @@ def test_container_has_required_tools_isolated(container_image: str): f"This indicates the tool is not properly packaged. " f"returncode: {result.returncode}, stdout: {result.stdout}, stderr: {result.stderr}" ) - assert ( - "Usage:" in result.stdout or "usage:" in result.stdout - ), f"sbctl --help output doesn't contain expected usage text: {result.stdout}" + assert "Usage:" in result.stdout or "usage:" in result.stdout, ( + f"sbctl --help output doesn't contain expected usage text: {result.stdout}" + ) elif tool_name == "kubectl": # Test kubectl exists and works @@ -93,9 +93,9 @@ def test_container_has_required_tools_isolated(container_image: str): f"This indicates the tool is not properly packaged. " f"returncode: {result.returncode}, stdout: {result.stdout}, stderr: {result.stderr}" ) - assert ( - "Client Version:" in result.stdout - ), f"kubectl version output doesn't contain expected version text: {result.stdout}" + assert "Client Version:" in result.stdout, ( + f"kubectl version output doesn't contain expected version text: {result.stdout}" + ) elif tool_name == "python3": # Test python3 exists and works @@ -122,9 +122,9 @@ def test_container_has_required_tools_isolated(container_image: str): f"This indicates the tool is not properly packaged. " f"returncode: {result.returncode}, stdout: {result.stdout}, stderr: {result.stderr}" ) - assert ( - "Python" in result.stdout - ), f"python3 --version output doesn't contain expected version text: {result.stdout}" + assert "Python" in result.stdout, ( + f"python3 --version output doesn't contain expected version text: {result.stdout}" + ) def test_container_bundle_initialization_isolated( @@ -330,6 +330,4 @@ def test_production_container_mcp_protocol(): assert "jsonrpc" in response, f"Invalid MCP response format: {response}" assert response.get("id") == "test-1", f"Response ID mismatch: {response}" except json.JSONDecodeError as e: - pytest.fail( - f"Container returned invalid JSON response: {result.stdout}, error: {e}" - ) + pytest.fail(f"Container returned invalid JSON response: {result.stdout}, error: {e}") diff --git a/tests/e2e/test_container_shutdown_reliability.py b/tests/e2e/test_container_shutdown_reliability.py index 589080a..a1d9a10 100644 --- a/tests/e2e/test_container_shutdown_reliability.py +++ b/tests/e2e/test_container_shutdown_reliability.py @@ -135,9 +135,7 @@ def test_stdio_mode_sigterm_shutdown(self): ) # Should exit cleanly without Python runtime errors - assert ( - "Fatal Python error" not in stderr - ), f"Python runtime error detected: {stderr}" + assert "Fatal Python error" not in stderr, f"Python runtime error detected: {stderr}" assert "_enter_buffered_busy" not in stderr assert "could not acquire lock" not in stderr @@ -291,9 +289,7 @@ def test_container_like_environment_full_lifecycle(self): ) + "\n", # List tools - json.dumps( - {"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2} - ) + json.dumps({"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2}) + "\n", ] diff --git a/tests/e2e/test_direct_tool_integration.py b/tests/e2e/test_direct_tool_integration.py index 0251a80..09ec909 100644 --- a/tests/e2e/test_direct_tool_integration.py +++ b/tests/e2e/test_direct_tool_integration.py @@ -101,12 +101,8 @@ async def test_initialize_bundle_tool_direct(self, test_bundle_copy): try: result_data = json.loads(result_text) - assert ( - "bundle_id" in result_data - ), f"Response should contain bundle_id: {result_text}" - assert ( - "status" in result_data - ), f"Response should contain status: {result_text}" + assert "bundle_id" in result_data, f"Response should contain bundle_id: {result_text}" + assert "status" in result_data, f"Response should contain status: {result_text}" except json.JSONDecodeError: # If not JSON, check for success indicators in text assert any( @@ -128,21 +124,16 @@ async def test_list_available_bundles_tool_direct(self, test_bundle_copy): # The bundle should be listed since it exists in the storage directory # If not found, it might be a valid case where the bundle isn't recognized - if ( - bundle_name not in bundles_text - and "No support bundles found" in bundles_text - ): + if bundle_name not in bundles_text and "No support bundles found" in bundles_text: # This is acceptable - bundle might need to be in a specific format - print( - "Bundle not automatically detected, this is expected for test bundles" + print("Bundle not automatically detected, this is expected for test bundles") + assert "support bundles" in bundles_text.lower(), ( + f"Should mention bundles: {bundles_text}" ) - assert ( - "support bundles" in bundles_text.lower() - ), f"Should mention bundles: {bundles_text}" else: - assert ( - bundle_name in bundles_text - ), f"Bundle {bundle_name} should appear in list: {bundles_text}" + assert bundle_name in bundles_text, ( + f"Bundle {bundle_name} should appear in list: {bundles_text}" + ) @pytest.mark.asyncio async def test_file_operations_direct(self, test_bundle_copy): @@ -159,9 +150,7 @@ async def test_file_operations_direct(self, test_bundle_copy): files_text = list_content[0].text # Should have some files in the bundle - assert ( - len(files_text.strip()) > 0 - ), f"File listing should not be empty: {files_text}" + assert len(files_text.strip()) > 0, f"File listing should not be empty: {files_text}" # Look for a file to read (try common bundle file patterns) import json @@ -178,14 +167,12 @@ async def test_file_operations_direct(self, test_bundle_copy): if file_path: read_args = ReadFileArgs(path=file_path) read_content = await read_file(read_args) - assert ( - len(read_content) > 0 - ), f"Should be able to read file {file_path}" + assert len(read_content) > 0, f"Should be able to read file {file_path}" except json.JSONDecodeError: # If not JSON, just verify we got some text output - assert ( - "file" in files_text.lower() or "directory" in files_text.lower() - ), f"File listing should mention files or directories: {files_text}" + assert "file" in files_text.lower() or "directory" in files_text.lower(), ( + f"File listing should mention files or directories: {files_text}" + ) @pytest.mark.asyncio async def test_grep_functionality_direct(self, test_bundle_copy): @@ -218,14 +205,10 @@ async def test_kubectl_tool_direct(self, test_bundle_copy): await initialize_bundle(init_args) # Test kubectl version command (should work even with limited cluster) - kubectl_args = KubectlCommandArgs( - command="version --client", timeout=10, json_output=False - ) + kubectl_args = KubectlCommandArgs(command="version --client", timeout=10, json_output=False) try: - kubectl_content = await asyncio.wait_for( - kubectl(kubectl_args), timeout=15.0 - ) + kubectl_content = await asyncio.wait_for(kubectl(kubectl_args), timeout=15.0) assert len(kubectl_content) > 0, "Should have kubectl output" kubectl_text = kubectl_content[0].text diff --git a/tests/e2e/test_mcp_protocol_integration.py b/tests/e2e/test_mcp_protocol_integration.py index dfa7255..93a24ce 100644 --- a/tests/e2e/test_mcp_protocol_integration.py +++ b/tests/e2e/test_mcp_protocol_integration.py @@ -39,9 +39,7 @@ def temp_bundle_dir(): class TestMCPProtocolLifecycle: """Test complete MCP server lifecycle via JSON-RPC protocol.""" - async def test_server_startup_and_initialization( - self, temp_bundle_dir, test_bundle_path - ): + async def test_server_startup_and_initialization(self, temp_bundle_dir, test_bundle_path): """ Test server startup and MCP initialization handshake. @@ -141,9 +139,9 @@ async def test_tool_discovery_via_protocol(self, temp_bundle_dir, test_bundle_pa } actual_tools = {tool["name"] for tool in tools} - assert expected_tools.issubset( - actual_tools - ), f"Missing expected tools. Expected: {expected_tools}, Actual: {actual_tools}" + assert expected_tools.issubset(actual_tools), ( + f"Missing expected tools. Expected: {expected_tools}, Actual: {actual_tools}" + ) # Verify each tool has required properties for tool in tools: @@ -173,33 +171,26 @@ async def test_bundle_loading_via_initialize_bundle_tool( await client.initialize_mcp() # Test bundle loading via MCP tool call - content = await client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ) + content = await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) # Verify successful bundle loading assert len(content) > 0, "initialize_bundle should return content" result_text = content[0].get("text", "") - assert ( - "successfully" in result_text.lower() - or "initialized" in result_text.lower() - ), f"Bundle initialization appears to have failed. Response: {result_text}" + assert "successfully" in result_text.lower() or "initialized" in result_text.lower(), ( + f"Bundle initialization appears to have failed. Response: {result_text}" + ) # Verify bundle is now accessible via list_available_bundles bundles_content = await client.call_tool("list_available_bundles") - assert ( - len(bundles_content) > 0 - ), "Should have at least one bundle after initialization" + assert len(bundles_content) > 0, "Should have at least one bundle after initialization" bundles_text = bundles_content[0].get("text", "") - assert ( - bundle_name in bundles_text - ), f"Loaded bundle {bundle_name} should appear in bundle list: {bundles_text}" + assert bundle_name in bundles_text, ( + f"Loaded bundle {bundle_name} should appear in bundle list: {bundles_text}" + ) - async def test_file_operations_via_protocol( - self, temp_bundle_dir, test_bundle_path - ): + async def test_file_operations_via_protocol(self, temp_bundle_dir, test_bundle_path): """ Test file operations (list_files, read_file) via MCP protocol. @@ -216,9 +207,7 @@ async def test_file_operations_via_protocol( async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ) + await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) # Test file listing via protocol files_content = await client.call_tool("list_files", {"path": "."}) @@ -234,15 +223,11 @@ async def test_file_operations_via_protocol( # Extract file names from the listing files_text = files_list[0].get("text", "") - file_lines = [ - line.strip() for line in files_text.split("\n") if line.strip() - ] + file_lines = [line.strip() for line in files_text.split("\n") if line.strip()] assert len(file_lines) > 0, "Should have at least one file in the bundle" # Get the first file for testing (remove any tree symbols) - first_file = file_lines[0].split()[ - -1 - ] # Take the last part after any tree symbols + first_file = file_lines[0].split()[-1] # Take the last part after any tree symbols # Test reading the actual file that exists in the bundle file_content = await client.call_tool("read_file", {"path": first_file}) @@ -252,9 +237,7 @@ async def test_file_operations_via_protocol( # Some files might be binary or empty, just verify we got a response assert content_text is not None, "File content should be retrievable" - async def test_grep_functionality_via_protocol( - self, temp_bundle_dir, test_bundle_path - ): + async def test_grep_functionality_via_protocol(self, temp_bundle_dir, test_bundle_path): """ Test file searching via the grep_files MCP tool. @@ -270,15 +253,11 @@ async def test_grep_functionality_via_protocol( async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ) + await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) # Test grep functionality via protocol # Search for a common term that should exist in Kubernetes bundles - grep_content = await client.call_tool( - "grep_files", {"pattern": "kind:", "path": "."} - ) + grep_content = await client.call_tool("grep_files", {"pattern": "kind:", "path": "."}) assert len(grep_content) > 0, "grep_files should return content" @@ -302,14 +281,10 @@ async def test_kubectl_tool_via_protocol(self, temp_bundle_dir, test_bundle_path async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ) + await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) # Test basic kubectl command via protocol - kubectl_content = await client.call_tool( - "kubectl", {"command": "get nodes"} - ) + kubectl_content = await client.call_tool("kubectl", {"command": "get nodes"}) assert len(kubectl_content) > 0, "kubectl should return content" @@ -319,9 +294,7 @@ async def test_kubectl_tool_via_protocol(self, temp_bundle_dir, test_bundle_path # The command might fail (no nodes in test bundle), but should not crash # We just verify the protocol layer works correctly - async def test_kubectl_exec_handling_via_protocol( - self, temp_bundle_dir, test_bundle_path - ): + async def test_kubectl_exec_handling_via_protocol(self, temp_bundle_dir, test_bundle_path): """ Test kubectl exec command handling via MCP protocol. @@ -338,23 +311,17 @@ async def test_kubectl_exec_handling_via_protocol( async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ) + await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) # Test kubectl exec command via protocol - this should not crash the server kubectl_content = await client.call_tool( "kubectl", {"command": "exec -it some-pod -- /bin/bash"} ) - assert ( - len(kubectl_content) > 0 - ), "kubectl exec should return content (even if error)" + assert len(kubectl_content) > 0, "kubectl exec should return content (even if error)" kubectl_text = kubectl_content[0].get("text", "") - assert isinstance( - kubectl_text, str - ), "kubectl exec result should be a string" + assert isinstance(kubectl_text, str), "kubectl exec result should be a string" # The command will likely fail, but should return a sensible error message # and not crash the server. The key is that the server doesn't crash @@ -362,20 +329,18 @@ async def test_kubectl_exec_handling_via_protocol( # It's OK if kubectl exec fails - the important thing is it doesn't crash # and returns a meaningful response - assert ( - len(kubectl_text.strip()) > 0 - ), "kubectl exec should return some response, even if it's an error message" + assert len(kubectl_text.strip()) > 0, ( + "kubectl exec should return some response, even if it's an error message" + ) # Verify server is still responsive after kubectl exec # by making another tool call tools_response = await client.send_request("tools/list") - assert ( - "result" in tools_response - ), "Server should still be responsive after kubectl exec" + assert "result" in tools_response, ( + "Server should still be responsive after kubectl exec" + ) - async def test_kubectl_interactive_commands_handling( - self, temp_bundle_dir, test_bundle_path - ): + async def test_kubectl_interactive_commands_handling(self, temp_bundle_dir, test_bundle_path): """ Test that interactive kubectl commands are handled gracefully. @@ -395,9 +360,7 @@ async def test_kubectl_interactive_commands_handling( async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ) + await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) # Test various potentially problematic kubectl commands problematic_commands = [ @@ -414,18 +377,14 @@ async def test_kubectl_interactive_commands_handling( assert len(kubectl_content) > 0, f"kubectl {cmd} should return content" kubectl_text = kubectl_content[0].get("text", "") - assert isinstance( - kubectl_text, str - ), f"kubectl {cmd} result should be a string" - assert ( - len(kubectl_text.strip()) > 0 - ), f"kubectl {cmd} should return some response" + assert isinstance(kubectl_text, str), f"kubectl {cmd} result should be a string" + assert len(kubectl_text.strip()) > 0, f"kubectl {cmd} should return some response" # Verify server is still responsive after each command tools_response = await client.send_request("tools/list") - assert ( - "result" in tools_response - ), f"Server should be responsive after kubectl {cmd}" + assert "result" in tools_response, ( + f"Server should be responsive after kubectl {cmd}" + ) class TestMCPProtocolErrorHandling: @@ -455,17 +414,15 @@ async def test_bundle_loading_failure_via_protocol(self, temp_bundle_dir): # If it doesn't throw, check that error is reported in content assert len(content) > 0, "Should return error content" result_text = content[0].get("text", "") - assert ( - "error" in result_text.lower() or "not found" in result_text.lower() - ), f"Should report error for non-existent bundle: {result_text}" + assert "error" in result_text.lower() or "not found" in result_text.lower(), ( + f"Should report error for non-existent bundle: {result_text}" + ) except RuntimeError as e: # It's also acceptable for this to raise an RPC error assert "error" in str(e).lower(), f"Error should be descriptive: {e}" - async def test_file_access_error_via_protocol( - self, temp_bundle_dir, test_bundle_path - ): + async def test_file_access_error_via_protocol(self, temp_bundle_dir, test_bundle_path): """ Test file access error handling via MCP protocol. @@ -482,9 +439,7 @@ async def test_file_access_error_via_protocol( async with MCPTestClient(bundle_dir=temp_bundle_dir, env=env) as client: # Initialize and load bundle await client.initialize_mcp() - await client.call_tool( - "initialize_bundle", {"source": str(test_bundle_copy)} - ) + await client.call_tool("initialize_bundle", {"source": str(test_bundle_copy)}) # Try to read non-existent file try: @@ -495,16 +450,15 @@ async def test_file_access_error_via_protocol( # Should either throw or return error in content if len(content) > 0: result_text = content[0].get("text", "") - assert ( - "error" in result_text.lower() - or "not found" in result_text.lower() - ), f"Should report error for non-existent file: {result_text}" + assert "error" in result_text.lower() or "not found" in result_text.lower(), ( + f"Should report error for non-existent file: {result_text}" + ) except RuntimeError as e: # It's also acceptable for this to raise an RPC error - assert ( - "error" in str(e).lower() or "not found" in str(e).lower() - ), f"Error should be descriptive: {e}" + assert "error" in str(e).lower() or "not found" in str(e).lower(), ( + f"Error should be descriptive: {e}" + ) async def test_invalid_tool_call_via_protocol(self, temp_bundle_dir): """ @@ -521,9 +475,7 @@ async def test_invalid_tool_call_via_protocol(self, temp_bundle_dir): # Try to call non-existent tool try: - response = await client.send_request( - "tools/call", {"name": "nonexistent_tool"} - ) + response = await client.send_request("tools/call", {"name": "nonexistent_tool"}) # Should not reach here - should get an error response pytest.fail(f"Expected error for non-existent tool, got: {response}") @@ -562,9 +514,7 @@ async def test_protocol_robustness(self, temp_bundle_dir): class TestMCPProtocolCompleteWorkflow: """Test complete workflow combining all MCP tools via protocol.""" - async def test_complete_bundle_analysis_workflow( - self, temp_bundle_dir, test_bundle_path - ): + async def test_complete_bundle_analysis_workflow(self, temp_bundle_dir, test_bundle_path): """ Test complete bundle analysis workflow via MCP protocol. @@ -611,9 +561,7 @@ async def test_complete_bundle_analysis_workflow( assert len(grep_result) > 0 # Step 6: Try kubectl command - kubectl_result = await client.call_tool( - "kubectl", {"command": "version --client"} - ) + kubectl_result = await client.call_tool("kubectl", {"command": "version --client"}) assert len(kubectl_result) > 0 # All steps completed successfully via MCP protocol diff --git a/tests/e2e/test_non_container.py b/tests/e2e/test_non_container.py index ed54491..572e663 100644 --- a/tests/e2e/test_non_container.py +++ b/tests/e2e/test_non_container.py @@ -26,9 +26,7 @@ def test_cli_module_exists(): try: from mcp_server_troubleshoot import cli - assert callable( - getattr(cli, "main", None) - ), "CLI module does not have a main function" + assert callable(getattr(cli, "main", None)), "CLI module does not have a main function" except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.cli module") @@ -38,9 +36,7 @@ def test_bundle_module_exists(): try: from mcp_server_troubleshoot import bundle - assert hasattr( - bundle, "BundleManager" - ), "Bundle module does not have BundleManager class" + assert hasattr(bundle, "BundleManager"), "Bundle module does not have BundleManager class" except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.bundle module") @@ -50,9 +46,7 @@ def test_files_module_exists(): try: from mcp_server_troubleshoot import files - assert hasattr( - files, "FileExplorer" - ), "Files module does not have FileExplorer class" + assert hasattr(files, "FileExplorer"), "Files module does not have FileExplorer class" except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.files module") @@ -62,9 +56,9 @@ def test_kubectl_module_exists(): try: from mcp_server_troubleshoot import kubectl - assert hasattr( - kubectl, "KubectlExecutor" - ), "Kubectl module does not have KubectlExecutor class" + assert hasattr(kubectl, "KubectlExecutor"), ( + "Kubectl module does not have KubectlExecutor class" + ) except ImportError: pytest.fail("Failed to import mcp_server_troubleshoot.kubectl module") @@ -90,12 +84,12 @@ def test_configuration_loading(): "log_level": "INFO", } # Test we can load configuration functions - assert hasattr( - config, "get_recommended_client_config" - ), "Config module missing get_recommended_client_config" - assert hasattr( - config, "load_config_from_path" - ), "Config module missing load_config_from_path" + assert hasattr(config, "get_recommended_client_config"), ( + "Config module missing get_recommended_client_config" + ) + assert hasattr(config, "load_config_from_path"), ( + "Config module missing load_config_from_path" + ) # Verify the test config values are as expected bundle_storage = test_config["bundle_storage"] @@ -129,9 +123,9 @@ def test_version_command(): check=False, ) assert result.returncode == 0, f"Version command failed with: {result.stderr}" - assert ( - "version" in result.stdout.lower() or "version" in result.stderr.lower() - ), "Version information not found in output" + assert "version" in result.stdout.lower() or "version" in result.stderr.lower(), ( + "Version information not found in output" + ) @pytest.mark.asyncio diff --git a/tests/e2e/test_podman.py b/tests/e2e/test_podman.py index 20b7095..c5518e4 100644 --- a/tests/e2e/test_podman.py +++ b/tests/e2e/test_podman.py @@ -106,9 +106,7 @@ def test_podman_availability() -> None: print(f"Using Podman version: {result.stdout.strip()}") -def test_basic_podman_run( - container_image: str, container_name: str, temp_bundle_dir: Path -) -> None: +def test_basic_podman_run(container_image: str, container_name: str, temp_bundle_dir: Path) -> None: """Test that the Podman container runs and exits successfully.""" result = subprocess.run( [ @@ -170,9 +168,7 @@ def test_installed_tools(container_image: str, container_name: str) -> None: assert result.stdout.strip(), f"{tool} path is empty" -def test_help_command( - container_image: str, container_name: str, temp_bundle_dir: Path -) -> None: +def test_help_command(container_image: str, container_name: str, temp_bundle_dir: Path) -> None: """Test that the application's help command works.""" result = subprocess.run( [ @@ -201,9 +197,7 @@ def test_help_command( assert "usage:" in combined_output.lower(), "Application help command failed" -def test_version_command( - container_image: str, container_name: str, temp_bundle_dir: Path -) -> None: +def test_version_command(container_image: str, container_name: str, temp_bundle_dir: Path) -> None: """Test that the application's version command works.""" result = subprocess.run( [ @@ -272,9 +266,7 @@ def test_process_dummy_bundle( # Verify the application CLI works assert result.returncode == 0, f"Failed to run container: {result.stderr}" - assert ( - "usage:" in (result.stdout + result.stderr).lower() - ), "Application CLI is not working" + assert "usage:" in (result.stdout + result.stderr).lower(), "Application CLI is not working" if __name__ == "__main__": diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 7e35dcd..4ef97c7 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -50,12 +50,8 @@ def assert_api_response_valid( """ assert isinstance(response, list), "Response should be a list" assert len(response) > 0, "Response should not be empty" - assert hasattr( - response[0], "type" - ), "Response item should have 'type' attribute" - assert ( - response[0].type == expected_type - ), f"Response type should be '{expected_type}'" + assert hasattr(response[0], "type"), "Response item should have 'type' attribute" + assert response[0].type == expected_type, f"Response type should be '{expected_type}'" if contains and hasattr(response[0], "text"): for text in contains: @@ -76,9 +72,9 @@ def assert_object_matches_attrs(obj: Any, expected_attrs: Dict[str, Any]) -> Non for attr, expected in expected_attrs.items(): assert hasattr(obj, attr), f"Object should have attribute '{attr}'" actual = getattr(obj, attr) - assert ( - actual == expected - ), f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" + assert actual == expected, ( + f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" + ) @staticmethod async def assert_asyncio_timeout(coro, timeout: float = 0.1) -> None: diff --git a/tests/integration/mcp_test_utils.py b/tests/integration/mcp_test_utils.py index faade11..d882254 100644 --- a/tests/integration/mcp_test_utils.py +++ b/tests/integration/mcp_test_utils.py @@ -25,9 +25,7 @@ class MCPTestClient: with it using the JSON-RPC 2.0 protocol over stdio transport. """ - def __init__( - self, bundle_dir: Optional[Path] = None, env: Optional[Dict[str, str]] = None - ): + def __init__(self, bundle_dir: Optional[Path] = None, env: Optional[Dict[str, str]] = None): """ Initialize the MCP test client. @@ -202,9 +200,7 @@ async def send_request( raise RuntimeError(f"Response is not a JSON object: {response}") if response.get("jsonrpc") != "2.0": - raise RuntimeError( - f"Invalid JSON-RPC version: {response.get('jsonrpc')}" - ) + raise RuntimeError(f"Invalid JSON-RPC version: {response.get('jsonrpc')}") if response.get("id") != request_id: raise RuntimeError( @@ -225,9 +221,7 @@ async def send_request( logger.error(f"Error during RPC communication: {e}") raise - async def send_notification( - self, method: str, params: Optional[Dict[str, Any]] = None - ) -> None: + async def send_notification(self, method: str, params: Optional[Dict[str, Any]] = None) -> None: """ Send JSON-RPC notification (no response expected). @@ -265,9 +259,7 @@ async def send_notification( logger.error(f"Error sending notification: {e}") raise - async def initialize_mcp( - self, client_info: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + async def initialize_mcp(self, client_info: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Send MCP initialize request to establish connection. @@ -280,8 +272,7 @@ async def initialize_mcp( params = { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, - "clientInfo": client_info - or {"name": "mcp-test-client", "version": "1.0.0"}, + "clientInfo": client_info or {"name": "mcp-test-client", "version": "1.0.0"}, } response = await self.send_request("initialize", params) diff --git a/tests/integration/test_api_server_lifecycle.py b/tests/integration/test_api_server_lifecycle.py index f8dd5d1..3db57f6 100644 --- a/tests/integration/test_api_server_lifecycle.py +++ b/tests/integration/test_api_server_lifecycle.py @@ -33,9 +33,7 @@ async def bundle_manager(self): def test_bundle_path(self): """Path to test bundle fixture.""" return ( - Path(__file__).parent.parent - / "fixtures" - / "support-bundle-2025-04-11T14_05_31.tar.gz" + Path(__file__).parent.parent / "fixtures" / "support-bundle-2025-04-11T14_05_31.tar.gz" ) @pytest.mark.asyncio @@ -105,9 +103,9 @@ async def test_api_server_availability_check( result = await kubectl_executor.execute("get namespaces", json_output=True) # Verify we got a successful response - assert ( - result.exit_code == 0 - ), f"kubectl command failed with exit code {result.exit_code}: {result.stderr}" + assert result.exit_code == 0, ( + f"kubectl command failed with exit code {result.exit_code}: {result.stderr}" + ) # Verify we got valid JSON output assert result.is_json, "Expected JSON output from kubectl get namespaces" @@ -197,19 +195,14 @@ async def test_cleanup_verification( assert bundle_manager.sbctl_process is None # Verify bundle directory is cleaned up (if in temp directory) - if ( - "/tmp" in str(initial_bundle_path) - or "temp" in str(initial_bundle_path).lower() - ): + if "/tmp" in str(initial_bundle_path) or "temp" in str(initial_bundle_path).lower(): assert not initial_bundle_path.exists() # Verify process is terminated if initial_process_pid: try: os.kill(initial_process_pid, 0) - pytest.fail( - f"Process {initial_process_pid} should have been terminated" - ) + pytest.fail(f"Process {initial_process_pid} should have been terminated") except OSError: # Process is gone, which is expected pass @@ -219,9 +212,7 @@ async def test_cleanup_verification( pid_files = list(temp_dir.glob("**/mock_sbctl.pid")) # Filter to only our test files relevant_pid_files = [f for f in pid_files if "troubleshoot" in str(f.parent)] - assert ( - len(relevant_pid_files) == 0 - ), f"Found leftover PID files: {relevant_pid_files}" + assert len(relevant_pid_files) == 0, f"Found leftover PID files: {relevant_pid_files}" @pytest.mark.asyncio @pytest.mark.integration @@ -244,16 +235,12 @@ async def test_multiple_initialization_cleanup( # First initialization result1 = await bundle_manager.initialize_bundle(str(test_bundle_path)) assert result1.initialized is True - first_pid = ( - bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None - ) + first_pid = bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None # Second initialization should clean up first result2 = await bundle_manager.initialize_bundle(str(test_bundle_path)) assert result2.initialized is True - second_pid = ( - bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None - ) + second_pid = bundle_manager.sbctl_process.pid if bundle_manager.sbctl_process else None # Verify new process is different (or at least that old one is gone) if first_pid and second_pid: @@ -261,16 +248,12 @@ async def test_multiple_initialization_cleanup( # Different PIDs - verify first process is gone try: os.kill(first_pid, 0) - pytest.fail( - f"First process {first_pid} should have been terminated" - ) + pytest.fail(f"First process {first_pid} should have been terminated") except OSError: # Expected - process should be gone pass - async def _collect_diagnostics( - self, bundle_manager: BundleManager - ) -> Dict[str, Any]: + async def _collect_diagnostics(self, bundle_manager: BundleManager) -> Dict[str, Any]: """Collect diagnostic information from the running system.""" diagnostics = {} @@ -345,8 +328,7 @@ async def _collect_diagnostics( "diagnostic_collected_at": time.time(), "bundle_initialized_at": ( bundle_manager.active_bundle.path.stat().st_mtime - if bundle_manager.active_bundle - and bundle_manager.active_bundle.path.exists() + if bundle_manager.active_bundle and bundle_manager.active_bundle.path.exists() else None ), } diff --git a/tests/integration/test_mcp_protocol_errors.py b/tests/integration/test_mcp_protocol_errors.py index 3f917b8..c46f21e 100644 --- a/tests/integration/test_mcp_protocol_errors.py +++ b/tests/integration/test_mcp_protocol_errors.py @@ -146,9 +146,7 @@ async def test_rapid_initialization_requests(self, mcp_client): ) # Either some succeed or all fail gracefully - assert ( - valid_responses >= 0 - ) # This will always pass but documents the expectation + assert valid_responses >= 0 # This will always pass but documents the expectation except Exception as e: # Rapid requests may cause issues - this is acceptable for robustness testing diff --git a/tests/integration/test_real_bundle.py b/tests/integration/test_real_bundle.py index d5a3856..d44be03 100644 --- a/tests/integration/test_real_bundle.py +++ b/tests/integration/test_real_bundle.py @@ -74,23 +74,17 @@ def test_sbctl_help_behavior(test_support_bundle): # Verify sbctl is available (basic behavior) # Log which sbctl is being used for debugging result = subprocess.run(["which", "sbctl"], capture_output=True, text=True) - assert ( - result.returncode == 0 - ), "sbctl command should be available (which sbctl failed)" + assert result.returncode == 0, "sbctl command should be available (which sbctl failed)" print(f"Using sbctl at: {result.stdout.strip()}") # Also check if executable permission is set sbctl_path = result.stdout.strip() if sbctl_path: - perm_result = subprocess.run( - ["ls", "-la", sbctl_path], capture_output=True, text=True - ) + perm_result = subprocess.run(["ls", "-la", sbctl_path], capture_output=True, text=True) print(f"sbctl permissions: {perm_result.stdout.strip()}") # Check help output behavior - help_result = subprocess.run( - ["sbctl", "--help"], capture_output=True, text=True, timeout=5 - ) + help_result = subprocess.run(["sbctl", "--help"], capture_output=True, text=True, timeout=5) # Verify the command ran successfully assert help_result.returncode == 0, "sbctl help command should succeed" @@ -127,15 +121,13 @@ def test_sbctl_help_behavior(test_support_bundle): ) # Verify help for serve is available - assert ( - serve_help_result.returncode == 0 - ), "sbctl serve help command should succeed" + assert serve_help_result.returncode == 0, "sbctl serve help command should succeed" # Verify serve help contains expected options serve_help_output = serve_help_result.stdout - assert ( - "--support-bundle-location" in serve_help_output - ), "Serve command should document bundle location option" + assert "--support-bundle-location" in serve_help_output, ( + "Serve command should document bundle location option" + ) @pytest.mark.asyncio @@ -154,9 +146,7 @@ async def test_bundle_lifecycle(bundle_manager_fixture): manager, real_bundle_path = bundle_manager_fixture # Act: Initialize the bundle - result = await asyncio.wait_for( - manager.initialize_bundle(str(real_bundle_path)), timeout=30.0 - ) + result = await asyncio.wait_for(manager.initialize_bundle(str(real_bundle_path)), timeout=30.0) # Assert: Verify functional behavior (not implementation details) assert result.initialized, "Bundle should be marked as initialized" @@ -166,9 +156,7 @@ async def test_bundle_lifecycle(bundle_manager_fixture): # Verify the bundle can be retrieved by the public API active_bundle = manager.get_active_bundle() assert active_bundle is not None, "Active bundle should be available" - assert ( - active_bundle.id == result.id - ), "Active bundle should match initialized bundle" + assert active_bundle.id == result.id, "Active bundle should match initialized bundle" # Verify API server functionality (behavior, not implementation) await manager.check_api_server_available() @@ -177,9 +165,9 @@ async def test_bundle_lifecycle(bundle_manager_fixture): # Test that re-initialization without force returns the same bundle second_result = await manager.initialize_bundle(str(real_bundle_path), force=False) - assert ( - second_result.id == result.id - ), "Re-initialization without force should return the same bundle" + assert second_result.id == result.id, ( + "Re-initialization without force should return the same bundle" + ) # Test that force re-initialization creates a new bundle force_result = await manager.initialize_bundle(str(real_bundle_path), force=True) @@ -226,18 +214,14 @@ async def test_bundle_initialization_workflow(bundle_manager_fixture, test_asser dir_contents = await explorer.list_files(first_dir, False) # Verify behavior - can list contents of subdirectory - assert ( - dir_contents is not None - ), "Should be able to list subdirectory contents" - assert isinstance( - dir_contents.entries, list - ), "Directory contents should be a list" + assert dir_contents is not None, "Should be able to list subdirectory contents" + assert isinstance(dir_contents.entries, list), "Directory contents should be a list" # TEST 3: Recursive listing behavior recursive_list = await explorer.list_files(first_dir, True) - assert ( - recursive_list.total_files + recursive_list.total_dirs > 0 - ), "Recursive listing should find files/dirs" + assert recursive_list.total_files + recursive_list.total_dirs > 0, ( + "Recursive listing should find files/dirs" + ) # TEST 4: File reading behavior # Find a file to read (we don't care which, just that we can read one) @@ -249,18 +233,14 @@ async def test_bundle_initialization_workflow(bundle_manager_fixture, test_asser # Verify behavior - can read file contents assert file_content is not None, "Should be able to read file contents" - assert ( - file_content.content is not None - ), "File content should not be None" + assert file_content.content is not None, "File content should not be None" # Check metadata (behavior we depend on) - assert ( - file_content.path == first_file - ), "File content should have correct path" + assert file_content.path == first_file, "File content should have correct path" # Note: We're checking for path existence, not name which might not be in all versions - assert hasattr( - file_content, "content" - ), "File content should have content attribute" + assert hasattr(file_content, "content"), ( + "File content should have content attribute" + ) @pytest.mark.asyncio @@ -293,21 +273,17 @@ async def test_bundle_manager_performance(bundle_manager_fixture): # Verify expected initialization behavior assert result.initialized, "Bundle should be marked as initialized" - assert ( - result.kubeconfig_path.exists() - ), "Initialization should create a kubeconfig file" - assert ( - duration < 45.0 - ), f"Initialization should complete in reasonable time (took {duration:.2f}s)" + assert result.kubeconfig_path.exists(), "Initialization should create a kubeconfig file" + assert duration < 45.0, ( + f"Initialization should complete in reasonable time (took {duration:.2f}s)" + ) # BEHAVIOR TEST 2: Verify kubeconfig has valid structure with open(result.kubeconfig_path, "r") as f: kubeconfig_content = f.read() # Check for essential kubeconfig fields that users and code depend on - assert ( - "clusters" in kubeconfig_content - ), "Kubeconfig should contain clusters section" + assert "clusters" in kubeconfig_content, "Kubeconfig should contain clusters section" assert "apiVersion" in kubeconfig_content, "Kubeconfig should contain API version" # BEHAVIOR TEST 3: The API server connection is attempted (we don't assert success @@ -318,28 +294,18 @@ async def test_bundle_manager_performance(bundle_manager_fixture): # This tests the observable behavior that getting the active bundle works active_bundle = manager.get_active_bundle() assert active_bundle is not None, "Manager should have an active bundle" - assert ( - active_bundle.id == result.id - ), "Active bundle should match initialized bundle" + assert active_bundle.id == result.id, "Active bundle should match initialized bundle" # BEHAVIOR TEST 5: Test diagnostic info is available - behavior users depend on diagnostics = await manager.get_diagnostic_info() - assert isinstance( - diagnostics, dict - ), "Diagnostic info should be available as a dictionary" - assert ( - "api_server_available" in diagnostics - ), "Diagnostics should report API server status" - assert ( - "bundle_initialized" in diagnostics - ), "Diagnostics should report bundle status" + assert isinstance(diagnostics, dict), "Diagnostic info should be available as a dictionary" + assert "api_server_available" in diagnostics, "Diagnostics should report API server status" + assert "bundle_initialized" in diagnostics, "Diagnostics should report bundle status" # Verify sbctl process was created successfully try: # Use ps to check for sbctl processes associated with this bundle - ps_result = subprocess.run( - ["ps", "-ef"], capture_output=True, text=True, timeout=5 - ) + ps_result = subprocess.run(["ps", "-ef"], capture_output=True, text=True, timeout=5) # There should be a sbctl process running for this bundle # We're checking behavior (process exists) not implementation (specific process args) diff --git a/tests/integration/test_server_lifecycle.py b/tests/integration/test_server_lifecycle.py index e7978df..b7f26af 100644 --- a/tests/integration/test_server_lifecycle.py +++ b/tests/integration/test_server_lifecycle.py @@ -88,9 +88,7 @@ async def test_server_startup_sequence_success(self, tmp_path: Path, mock_server os.environ.pop("MCP_BUNDLE_STORAGE", None) @pytest.mark.asyncio - async def test_server_startup_with_bundle_directory_scanning( - self, tmp_path: Path, mock_server - ): + async def test_server_startup_with_bundle_directory_scanning(self, tmp_path: Path, mock_server): """Test server startup automatically scans bundle directory for available bundles.""" bundle_dir = tmp_path / "bundles" bundle_dir.mkdir() @@ -153,9 +151,7 @@ async def test_server_startup_no_bundles_directory(self, mock_server): os.environ.pop("MCP_BUNDLE_STORAGE", None) @pytest.mark.asyncio - async def test_server_startup_invalid_bundles_handling( - self, tmp_path: Path, mock_server - ): + async def test_server_startup_invalid_bundles_handling(self, tmp_path: Path, mock_server): """Test server startup with invalid bundles (should handle errors gracefully).""" bundle_dir = tmp_path / "bundles" bundle_dir.mkdir() @@ -187,9 +183,7 @@ async def test_server_startup_invalid_bundles_handling( assert len(valid_bundles) == 1 # Test including invalid bundles - all_bundles = await bundle_manager.list_available_bundles( - include_invalid=True - ) + all_bundles = await bundle_manager.list_available_bundles(include_invalid=True) assert len(all_bundles) >= len(bundles) finally: @@ -298,9 +292,7 @@ def bundle_manager_context(self, tmp_path: Path): return bundle_dir @pytest.mark.asyncio - async def test_automatic_bundle_discovery_on_startup( - self, bundle_manager_context: Path - ): + async def test_automatic_bundle_discovery_on_startup(self, bundle_manager_context: Path): """Test automatic bundle discovery when server starts.""" # Create multiple bundles bundles_created = [] @@ -376,9 +368,7 @@ async def test_periodic_bundle_cleanup_function(self, tmp_path: Path): bundle_manager = BundleManager(bundle_dir) # Create cleanup task with short interval - cleanup_task = asyncio.create_task( - periodic_bundle_cleanup(bundle_manager, interval=0.1) - ) + cleanup_task = asyncio.create_task(periodic_bundle_cleanup(bundle_manager, interval=0.1)) # Let it run briefly await asyncio.sleep(0.3) diff --git a/tests/integration/test_shutdown_race_condition.py b/tests/integration/test_shutdown_race_condition.py index 029251c..64220fb 100644 --- a/tests/integration/test_shutdown_race_condition.py +++ b/tests/integration/test_shutdown_race_condition.py @@ -123,16 +123,12 @@ async def main_loop(): # The test "passes" if it reproduces the race condition # (which means the bug exists and needs to be fixed) - race_condition_found = any( - indicator in stderr for indicator in race_condition_indicators - ) + race_condition_found = any(indicator in stderr for indicator in race_condition_indicators) # The test now verifies that the race condition is FIXED if race_condition_found: # If we see the race condition, the fix didn't work - pytest.fail( - f"Race condition still present! Fix didn't work.\nstderr output:\n{stderr}" - ) + pytest.fail(f"Race condition still present! Fix didn't work.\nstderr output:\n{stderr}") else: # Good! No race condition detected print("No race condition detected - fix is working!") diff --git a/tests/integration/test_signal_handling_integration.py b/tests/integration/test_signal_handling_integration.py index b4ba491..31c0257 100644 --- a/tests/integration/test_signal_handling_integration.py +++ b/tests/integration/test_signal_handling_integration.py @@ -151,9 +151,7 @@ def test_sigterm_clean_shutdown(self): # Check what happened if return_code == -1: print(f"Process timed out. stderr:\n{stderr}") - pytest.fail( - "Process timed out, likely the signal handler did not properly exit" - ) + pytest.fail("Process timed out, likely the signal handler did not properly exit") # Should exit cleanly with code 0 or -15 (SIGTERM on Linux) assert return_code in ( @@ -324,9 +322,9 @@ async def main(): ) # Should not crash with Python runtime error - assert ( - "Fatal Python error" not in stderr - ), f"Race condition detected on iteration {i + 1}" + assert "Fatal Python error" not in stderr, ( + f"Race condition detected on iteration {i + 1}" + ) assert "_enter_buffered_busy" not in stderr def test_signal_with_resource_cleanup(self): diff --git a/tests/integration/test_subprocess_utilities_integration.py b/tests/integration/test_subprocess_utilities_integration.py index 14666e9..c1b3c52 100644 --- a/tests/integration/test_subprocess_utilities_integration.py +++ b/tests/integration/test_subprocess_utilities_integration.py @@ -33,9 +33,7 @@ async def test_subprocess_exec_with_cleanup_basic_command(): async def test_subprocess_exec_with_cleanup_timeout_handling(): """Test that subprocess_exec_with_cleanup properly handles timeouts.""" with pytest.raises(asyncio.TimeoutError): - await subprocess_exec_with_cleanup( - "sleep", "10", timeout=0.1 - ) # Very short timeout + await subprocess_exec_with_cleanup("sleep", "10", timeout=0.1) # Very short timeout # If we get here, the timeout was handled properly and cleanup occurred @@ -82,9 +80,7 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.01) # Verify no transport warnings occurred - assert ( - len(warnings_captured) == 0 - ), f"Transport warnings detected: {warnings_captured}" + assert len(warnings_captured) == 0, f"Transport warnings detected: {warnings_captured}" finally: # Restore original warning handler @@ -110,9 +106,7 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): "echo", f"test-{i}", timeout=5.0 ) assert returncode == 0, f"Command {i} should succeed" - assert ( - stdout == f"test-{i}\n".encode() - ), f"Should get expected output for {i}" + assert stdout == f"test-{i}\n".encode(), f"Should get expected output for {i}" # Force garbage collection to trigger any transport issues for _ in range(5): @@ -120,9 +114,7 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.01) # Verify no transport warnings occurred - assert ( - len(warnings_captured) == 0 - ), f"Transport warnings detected: {warnings_captured}" + assert len(warnings_captured) == 0, f"Transport warnings detected: {warnings_captured}" finally: warnings.showwarning = original_showwarning @@ -200,9 +192,7 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.01) # Verify no transport cleanup warnings - assert ( - len(warnings_captured) == 0 - ), f"Transport warnings detected: {warnings_captured}" + assert len(warnings_captured) == 0, f"Transport warnings detected: {warnings_captured}" finally: warnings.showwarning = original_showwarning diff --git a/tests/integration/test_tool_functions.py b/tests/integration/test_tool_functions.py index 220e175..bfa82ff 100644 --- a/tests/integration/test_tool_functions.py +++ b/tests/integration/test_tool_functions.py @@ -84,9 +84,7 @@ async def test_list_available_bundles_function(bundle_storage_dir): content_item = result[0] assert content_item.type == "text", "Content should be text type" - assert ( - "No support bundles found" in content_item.text - ), "Should indicate no bundles found" + assert "No support bundles found" in content_item.text, "Should indicate no bundles found" @pytest.mark.asyncio @@ -150,9 +148,7 @@ async def test_initialize_bundle_function_force_flag(bundle_storage_dir): assert len(result1) > 0, "First initialization should succeed" response1_text = result1[0].text - assert ( - "Bundle initialized" in response1_text - ), "First initialization should report success" + assert "Bundle initialized" in response1_text, "First initialization should report success" # Second initialization with force=True should also work args2 = InitializeBundleArgs(source=str(test_bundle), force=True) @@ -160,9 +156,7 @@ async def test_initialize_bundle_function_force_flag(bundle_storage_dir): assert len(result2) > 0, "Second initialization with force should succeed" response2_text = result2[0].text - assert ( - "Bundle initialized" in response2_text - ), "Second initialization should report success" + assert "Bundle initialized" in response2_text, "Second initialization should report success" @pytest.mark.asyncio @@ -188,9 +182,7 @@ async def test_initialize_bundle_validation_nonexistent_file(bundle_storage_dir) # Verify the error message indicates the file wasn't found error_msg = str(exc_info.value) - assert ( - "Bundle source not found" in error_msg - ), "Should indicate bundle source not found" + assert "Bundle source not found" in error_msg, "Should indicate bundle source not found" @pytest.mark.asyncio @@ -226,9 +218,7 @@ async def test_list_files_function_with_bundle(bundle_storage_dir): # Should contain file listing information assert "```json" in response_text, "Response should contain JSON data" - assert ( - "Listed files in" in response_text - ), "Response should indicate listing operation" + assert "Listed files in" in response_text, "Response should indicate listing operation" @pytest.mark.asyncio @@ -250,9 +240,9 @@ async def test_pydantic_validation_invalid_parameters(bundle_storage_dir): # Verify the error indicates path validation failure error_msg = str(exc_info.value) - assert ( - "Path cannot contain directory traversal" in error_msg - ), "Should indicate path validation error" + assert "Path cannot contain directory traversal" in error_msg, ( + "Should indicate path validation error" + ) @pytest.mark.asyncio @@ -320,9 +310,7 @@ async def test_read_file_function_execution(bundle_storage_dir): assert len(init_result) > 0, "Bundle initialization should succeed" # Try to read a common file via direct function call - read_args = ReadFileArgs( - path="cluster-info/version.json", start_line=0, num_lines=10 - ) + read_args = ReadFileArgs(path="cluster-info/version.json", start_line=0, num_lines=10) try: read_result = await read_file(read_args) diff --git a/tests/integration/test_url_fetch_auth.py b/tests/integration/test_url_fetch_auth.py index 1d12bc5..20ccb2a 100644 --- a/tests/integration/test_url_fetch_auth.py +++ b/tests/integration/test_url_fetch_auth.py @@ -53,9 +53,9 @@ def test_replicated_vendor_url_pattern_matching(self): match = REPLICATED_VENDOR_URL_PATTERN.match(url) if should_match: assert match is not None, f"URL {url} should match pattern" - assert ( - match.group(1) == expected_slug - ), f"Expected slug {expected_slug}, got {match.group(1)}" + assert match.group(1) == expected_slug, ( + f"Expected slug {expected_slug}, got {match.group(1)}" + ) else: assert match is None, f"URL {url} should not match pattern" @@ -123,9 +123,7 @@ async def test_replicated_api_401_error(self): ) with patch("httpx.AsyncClient") as mock_client: - mock_client.return_value.__aenter__.return_value.get.return_value = ( - mock_response - ) + mock_client.return_value.__aenter__.return_value.get.return_value = mock_response with pytest.raises( BundleDownloadError, @@ -149,9 +147,7 @@ async def test_replicated_api_404_error(self): ) with patch("httpx.AsyncClient") as mock_client: - mock_client.return_value.__aenter__.return_value.get.return_value = ( - mock_response - ) + mock_client.return_value.__aenter__.return_value.get.return_value = mock_response with pytest.raises( BundleDownloadError, @@ -191,9 +187,7 @@ async def test_download_size_limit_exceeded(self): mock_response.content_length = 2000000000 # 2GB with patch("aiohttp.ClientSession") as mock_session: - mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = ( - mock_response - ) + mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_response with pytest.raises( BundleDownloadError, match="Bundle size.*exceeds maximum allowed size" @@ -216,9 +210,7 @@ async def test_direct_download_auth_error(self): ) with patch("aiohttp.ClientSession") as mock_session: - mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = ( - mock_response - ) + mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_response with pytest.raises( BundleDownloadError, match="Failed to download bundle.*HTTP 401" @@ -265,16 +257,14 @@ def test_token_priority_sbctl_over_replicated(self): ): # This mirrors the logic in bundle.py line 397-398 token = os.environ.get("SBCTL_TOKEN") or os.environ.get("REPLICATED") - assert ( - token == "sbctl-token" - ), "SBCTL_TOKEN should take precedence over REPLICATED" + assert token == "sbctl-token", "SBCTL_TOKEN should take precedence over REPLICATED" with patch.dict(os.environ, {"REPLICATED": "replicated-token"}, clear=True): # When only REPLICATED is set (clear all env vars first) token = os.environ.get("SBCTL_TOKEN") or os.environ.get("REPLICATED") - assert ( - token == "replicated-token" - ), "REPLICATED should be used when SBCTL_TOKEN is not set" + assert token == "replicated-token", ( + "REPLICATED should be used when SBCTL_TOKEN is not set" + ) with patch.dict(os.environ, {}, clear=True): # When neither is set diff --git a/tests/test_all.py b/tests/test_all.py index 038825d..930a96f 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -18,6 +18,4 @@ def test_documentation(): """This is a placeholder test to explain test organization.""" - assert ( - True - ), "This test always passes. The real tests are in the respective directories." + assert True, "This test always passes. The real tests are in the respective directories." diff --git a/tests/test_utils/bundle_helpers.py b/tests/test_utils/bundle_helpers.py index a538ddd..41069f8 100644 --- a/tests/test_utils/bundle_helpers.py +++ b/tests/test_utils/bundle_helpers.py @@ -72,15 +72,11 @@ def create_test_bundle_structure(base_dir: Path) -> Dict[str, Path]: # Create sample host files os_info = host_info / "os-info.txt" - os_info.write_text( - "Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n" - ) + os_info.write_text("Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n") structure["os_info"] = os_info memory_info = host_info / "memory.txt" - memory_info.write_text( - "MemTotal: 8388608 kB\nMemFree: 4194304 kB\nMemAvailable: 6291456 kB\n" - ) + memory_info.write_text("MemTotal: 8388608 kB\nMemFree: 4194304 kB\nMemAvailable: 6291456 kB\n") structure["memory_info"] = memory_info # Create logs directory @@ -144,9 +140,7 @@ def create_host_only_bundle_structure(base_dir: Path) -> Dict[str, Path]: # Create comprehensive host files os_info = host_info / "os-info.txt" - os_info.write_text( - "Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n" - ) + os_info.write_text("Operating System: Linux\nKernel: 5.4.0\nDistribution: Ubuntu 20.04\n") structure["os_info"] = os_info processes = host_info / "processes.txt" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8351007..0e03404 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -56,12 +56,8 @@ def assert_api_response_valid( """ assert isinstance(response, list), "Response should be a list" assert len(response) > 0, "Response should not be empty" - assert hasattr( - response[0], "type" - ), "Response item should have 'type' attribute" - assert ( - response[0].type == expected_type - ), f"Response type should be '{expected_type}'" + assert hasattr(response[0], "type"), "Response item should have 'type' attribute" + assert response[0].type == expected_type, f"Response type should be '{expected_type}'" if contains and hasattr(response[0], "text"): for text in contains: @@ -82,9 +78,9 @@ def assert_object_matches_attrs(obj: Any, expected_attrs: Dict[str, Any]) -> Non for attr, expected in expected_attrs.items(): assert hasattr(obj, attr), f"Object should have attribute '{attr}'" actual = getattr(obj, attr) - assert ( - actual == expected - ), f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" + assert actual == expected, ( + f"Attribute '{attr}' value mismatch. Expected: {expected}, Got: {actual}" + ) @staticmethod async def assert_asyncio_timeout(coro, timeout: float = 0.1) -> None: @@ -283,9 +279,7 @@ def error_setup(tmp_path): # Create a mock process that fails error_process = AsyncMock() error_process.returncode = 1 - error_process.communicate = AsyncMock( - return_value=(b"", b"Command failed with an error") - ) + error_process.communicate = AsyncMock(return_value=(b"", b"Command failed with an error")) # Create a mock asyncio client session with errors error_session = AsyncMock() diff --git a/tests/unit/test_bundle.py b/tests/unit/test_bundle.py index e720763..e480803 100644 --- a/tests/unit/test_bundle.py +++ b/tests/unit/test_bundle.py @@ -82,9 +82,7 @@ async def test_bundle_manager_initialize_bundle_url(): manager._wait_for_initialization = AsyncMock() # Test initializing from a URL - result = await manager.initialize_bundle( - "https://example.com/bundle.tar.gz" - ) + result = await manager.initialize_bundle("https://example.com/bundle.tar.gz") # Verify the result assert isinstance(result, BundleMetadata) @@ -93,9 +91,7 @@ async def test_bundle_manager_initialize_bundle_url(): assert result.initialized is True # Verify the mocks were called - manager._download_bundle.assert_awaited_once_with( - "https://example.com/bundle.tar.gz" - ) + manager._download_bundle.assert_awaited_once_with("https://example.com/bundle.tar.gz") manager._initialize_with_sbctl.assert_awaited_once() @@ -164,9 +160,7 @@ async def test_bundle_manager_initialize_bundle_nonexistent(): REPLICATED_URL = "https://vendor.replicated.com/troubleshoot/analyze/2025-04-22@16:51" REPLICATED_SLUG = "2025-04-22@16:51" -REPLICATED_API_URL = ( - f"https://api.replicated.com/vendor/v3/supportbundle/{REPLICATED_SLUG}" -) +REPLICATED_API_URL = f"https://api.replicated.com/vendor/v3/supportbundle/{REPLICATED_SLUG}" SIGNED_URL = "https://signed.example.com/download?token=abc" @@ -234,9 +228,7 @@ async def async_iterator(): mock_aio_session.__aexit__ = AsyncMock(return_value=None) # Patch aiohttp.ClientSession to return our mock session - with patch( - "aiohttp.ClientSession", return_value=mock_aio_session - ) as mock_constructor: + with patch("aiohttp.ClientSession", return_value=mock_aio_session) as mock_constructor: # Yield the constructor, the session instance, and the response instance # This gives tests more flexibility for assertions yield mock_constructor, mock_aio_session, mock_aio_response @@ -248,9 +240,7 @@ async def test_bundle_manager_download_replicated_url_success_sbctl_token( ): """Test downloading from Replicated URL with SBCTL_TOKEN successfully.""" mock_httpx_constructor, mock_httpx_response = mock_httpx_client - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( - mock_aiohttp_download - ) + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download with tempfile.TemporaryDirectory() as temp_dir: bundle_dir = Path(temp_dir) @@ -265,9 +255,7 @@ async def test_bundle_manager_download_replicated_url_success_sbctl_token( _, kwargs = mock_httpx_constructor.call_args assert isinstance(kwargs.get("timeout"), httpx.Timeout) - mock_get_call = ( - mock_httpx_constructor.return_value.__aenter__.return_value.get - ) + mock_get_call = mock_httpx_constructor.return_value.__aenter__.return_value.get mock_get_call.assert_awaited_once_with( REPLICATED_API_URL, headers={ @@ -285,9 +273,7 @@ async def test_bundle_manager_download_replicated_url_success_sbctl_token( assert download_path.exists() # Assert new filename format # Replace both '@' and ':' for the assertion to match sanitization - safe_slug_for_assertion = REPLICATED_SLUG.replace("@", "_").replace( - ":", "_" - ) + safe_slug_for_assertion = REPLICATED_SLUG.replace("@", "_").replace(":", "_") expected_filename_part = f"replicated_bundle_{safe_slug_for_assertion}" assert download_path.name.startswith(expected_filename_part) assert download_path.read_bytes() == b"chunk1chunk2" @@ -299,24 +285,18 @@ async def test_bundle_manager_download_replicated_url_success_replicated_token( ): """Test downloading from Replicated URL with REPLICATED_TOKEN successfully.""" mock_httpx_constructor, mock_httpx_response = mock_httpx_client - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( - mock_aiohttp_download - ) + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download with tempfile.TemporaryDirectory() as temp_dir: bundle_dir = Path(temp_dir) manager = BundleManager(bundle_dir) # Only REPLICATED is set - with patch.dict( - os.environ, {"REPLICATED": "replicated_token_value"}, clear=True - ): + with patch.dict(os.environ, {"REPLICATED": "replicated_token_value"}, clear=True): await manager._download_bundle(REPLICATED_URL) # Verify httpx call used REPLICATED token - mock_get_call = ( - mock_httpx_constructor.return_value.__aenter__.return_value.get - ) + mock_get_call = mock_httpx_constructor.return_value.__aenter__.return_value.get mock_get_call.assert_awaited_once_with( REPLICATED_API_URL, headers={ @@ -335,9 +315,7 @@ async def test_bundle_manager_download_replicated_url_token_precedence( """Test SBCTL_TOKEN takes precedence over REPLICATED_TOKEN.""" mock_httpx_constructor, _ = mock_httpx_client # Unpack all three values from the fixture - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( - mock_aiohttp_download - ) + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download with tempfile.TemporaryDirectory() as temp_dir: bundle_dir = Path(temp_dir) @@ -357,9 +335,7 @@ async def test_bundle_manager_download_replicated_url_token_precedence( await manager._download_bundle(REPLICATED_URL) # Verify httpx call used SBCTL_TOKEN - mock_get_call = ( - mock_httpx_constructor.return_value.__aenter__.return_value.get - ) + mock_get_call = mock_httpx_constructor.return_value.__aenter__.return_value.get mock_get_call.assert_awaited_once_with( REPLICATED_API_URL, headers={ @@ -382,9 +358,7 @@ async def test_bundle_manager_download_replicated_url_missing_token(): await manager._download_bundle(REPLICATED_URL) # === START MODIFICATION === # Update assertion to match the exact error message and correct ENV name - expected_error_part = ( - "SBCTL_TOKEN or REPLICATED environment variable not set" - ) + expected_error_part = "SBCTL_TOKEN or REPLICATED environment variable not set" assert expected_error_part in str(excinfo.value) assert "Cannot download from Replicated Vendor Portal" in str(excinfo.value) # === END MODIFICATION === @@ -399,9 +373,7 @@ async def test_bundle_manager_download_replicated_url_api_401(mock_httpx_client) mock_response.status_code = 401 mock_response.text = "Unauthorized" # Ensure json() raises an error if called on non-200 status - mock_response.json.side_effect = json.JSONDecodeError( - "Mock JSON decode error", "", 0 - ) + mock_response.json.side_effect = json.JSONDecodeError("Mock JSON decode error", "", 0) # === END MODIFICATION === with tempfile.TemporaryDirectory() as temp_dir: @@ -415,9 +387,7 @@ async def test_bundle_manager_download_replicated_url_api_401(mock_httpx_client) await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === # The error should propagate from _get_replicated_signed_url - assert "Failed to authenticate with Replicated API (status 401)" in str( - excinfo.value - ) + assert "Failed to authenticate with Replicated API (status 401)" in str(excinfo.value) @pytest.mark.asyncio @@ -427,9 +397,7 @@ async def test_bundle_manager_download_replicated_url_api_404(mock_httpx_client) # === START MODIFICATION === mock_response.status_code = 404 mock_response.text = "Not Found" - mock_response.json.side_effect = json.JSONDecodeError( - "Mock JSON decode error", "", 0 - ) + mock_response.json.side_effect = json.JSONDecodeError("Mock JSON decode error", "", 0) # === END MODIFICATION === with tempfile.TemporaryDirectory() as temp_dir: @@ -442,9 +410,7 @@ async def test_bundle_manager_download_replicated_url_api_404(mock_httpx_client) # Call _download_bundle instead of _get_replicated_signed_url await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === - assert "Support bundle not found on Replicated Vendor Portal" in str( - excinfo.value - ) + assert "Support bundle not found on Replicated Vendor Portal" in str(excinfo.value) assert f"slug: {REPLICATED_SLUG}" in str(excinfo.value) @@ -457,9 +423,7 @@ async def test_bundle_manager_download_replicated_url_api_other_error( # === START MODIFICATION === mock_response.status_code = 500 mock_response.text = "Internal Server Error" - mock_response.json.side_effect = json.JSONDecodeError( - "Mock JSON decode error", "", 0 - ) + mock_response.json.side_effect = json.JSONDecodeError("Mock JSON decode error", "", 0) # === END MODIFICATION === with tempfile.TemporaryDirectory() as temp_dir: @@ -472,12 +436,8 @@ async def test_bundle_manager_download_replicated_url_api_other_error( # Call _download_bundle instead of _get_replicated_signed_url await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === - assert "Failed to get signed URL from Replicated API (status 500)" in str( - excinfo.value - ) - assert "Internal Server Error" in str( - excinfo.value - ) # Check response text included + assert "Failed to get signed URL from Replicated API (status 500)" in str(excinfo.value) + assert "Internal Server Error" in str(excinfo.value) # Check response text included @pytest.mark.asyncio @@ -505,9 +465,7 @@ async def test_bundle_manager_download_replicated_url_missing_signed_uri( # Call _download_bundle instead of _get_replicated_signed_url await manager._download_bundle(REPLICATED_URL) # === END MODIFICATION === - assert "Could not find 'signedUri' in Replicated API response" in str( - excinfo.value - ) + assert "Could not find 'signedUri' in Replicated API response" in str(excinfo.value) @pytest.mark.asyncio @@ -529,18 +487,14 @@ async def test_bundle_manager_download_replicated_url_network_error(): # Assert that the correct error (raised by the except httpx.RequestError block) is caught assert "Network error requesting signed URL" in str(excinfo.value) - assert "Network timeout" in str( - excinfo.value - ) # Check original error is included + assert "Network timeout" in str(excinfo.value) # Check original error is included # === END MODIFICATION === @pytest.mark.asyncio async def test_bundle_manager_download_non_replicated_url(mock_aiohttp_download): """Test that non-Replicated URLs are downloaded directly without API calls.""" - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( - mock_aiohttp_download - ) + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download non_replicated_url = "https://normal.example.com/bundle.tar.gz" with tempfile.TemporaryDirectory() as temp_dir: @@ -575,9 +529,7 @@ async def test_bundle_manager_download_bundle( ): # Use fixture as argument """Test that the bundle manager can download a non-Replicated bundle.""" # Unpack the fixture results - mock_aiohttp_constructor, mock_aio_session, mock_aio_response = ( - mock_aiohttp_download - ) + mock_aiohttp_constructor, mock_aio_session, mock_aio_response = mock_aiohttp_download non_replicated_url = "https://example.com/bundle.tar.gz" with tempfile.TemporaryDirectory() as temp_dir: @@ -920,9 +872,7 @@ async def test_bundle_manager_server_shutdown_cleanup(): manager._cleanup_active_bundle = AsyncMock() # Mock subprocess.run to avoid actual process operations - with patch( - "subprocess.run", return_value=MagicMock(returncode=0, stdout="", stderr="") - ): + with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="", stderr="")): # Call cleanup await manager.cleanup() @@ -941,9 +891,7 @@ async def test_bundle_manager_server_shutdown_cleanup(): mock_pkill_result.returncode = 0 # Mock subprocess to return our mock objects - with patch( - "subprocess.run", side_effect=[mock_ps_result, mock_pkill_result] - ): + with patch("subprocess.run", side_effect=[mock_ps_result, mock_pkill_result]): # Test with orphaned processes await manager.cleanup() @@ -1006,12 +954,8 @@ async def test_bundle_manager_host_only_bundle_detection(): assert args[1] == "serve" assert args[2] == "--support-bundle-location" # Verify bundle path is in the arguments - bundle_arg_found = any( - str(local_bundle_path) in str(arg) for arg in args - ) - assert ( - bundle_arg_found - ), f"Bundle path not found in subprocess args: {args}" + bundle_arg_found = any(str(local_bundle_path) in str(arg) for arg in args) + assert bundle_arg_found, f"Bundle path not found in subprocess args: {args}" # Note: We test regular bundles in the existing tests that already work properly diff --git a/tests/unit/test_bundle_cleanup_dependencies.py b/tests/unit/test_bundle_cleanup_dependencies.py index a159fb8..22997fb 100644 --- a/tests/unit/test_bundle_cleanup_dependencies.py +++ b/tests/unit/test_bundle_cleanup_dependencies.py @@ -70,9 +70,7 @@ async def test_bundle_cleanup_functional_dependency_validation(): print("✅ No missing dependencies detected in cleanup process") # Verify cleanup actually did something - assert ( - bundle_manager.active_bundle is None - ), "Bundle should be cleared after cleanup" + assert bundle_manager.active_bundle is None, "Bundle should be cleared after cleanup" except FileNotFoundError as e: # This is what we expect to see if dependencies are missing in container @@ -218,9 +216,7 @@ def test_container_environment_simulation(): print( f"✅ Confirmed {len(unavailable_commands)} commands unavailable: {unavailable_commands}" ) - print( - "✅ This environment would have broken the original ps/pkill subprocess calls" - ) + print("✅ This environment would have broken the original ps/pkill subprocess calls") print("✅ The psutil fix ensures cleanup works even without external commands") finally: diff --git a/tests/unit/test_components.py b/tests/unit/test_components.py index 299d34e..edd316a 100755 --- a/tests/unit/test_components.py +++ b/tests/unit/test_components.py @@ -65,9 +65,7 @@ async def test_bundle_initialization(mock_command_environment, fixtures_dir): try: # Initialize the bundle logger.info("Initializing bundle...") - metadata = await bundle_manager.initialize_bundle( - str(test_bundle_copy), force=True - ) + metadata = await bundle_manager.initialize_bundle(str(test_bundle_copy), force=True) # Verify expected behavior assert metadata.initialized, "Bundle should be marked as initialized" @@ -75,9 +73,9 @@ async def test_bundle_initialization(mock_command_environment, fixtures_dir): # Verify diagnostic information diagnostics = await bundle_manager.get_diagnostic_info() - assert diagnostics[ - "bundle_initialized" - ], "Bundle should be marked as initialized in diagnostics" + assert diagnostics["bundle_initialized"], ( + "Bundle should be marked as initialized in diagnostics" + ) # Clean up the bundle manager await bundle_manager.cleanup() @@ -127,13 +125,9 @@ async def test_kubectl_execution(mock_command_environment, fixtures_dir): try: # Initialize the bundle first - metadata = await bundle_manager.initialize_bundle( - str(test_bundle_copy), force=True - ) + metadata = await bundle_manager.initialize_bundle(str(test_bundle_copy), force=True) assert metadata.initialized, "Bundle should be initialized successfully" - assert ( - metadata.kubeconfig_path.exists() - ), "Kubeconfig should exist after initialization" + assert metadata.kubeconfig_path.exists(), "Kubeconfig should exist after initialization" # Set KUBECONFIG environment variable for kubectl os.environ["KUBECONFIG"] = str(metadata.kubeconfig_path) @@ -150,14 +144,10 @@ async def test_kubectl_execution(mock_command_environment, fixtures_dir): assert proc.returncode == 0, "kubectl should be available in PATH" # Now run a command with the executor - result = await asyncio.wait_for( - kubectl_executor.execute("get nodes"), timeout=10.0 - ) + result = await asyncio.wait_for(kubectl_executor.execute("get nodes"), timeout=10.0) # Verify the command result behavior - assert ( - result.exit_code == 0 - ), f"Command should succeed, got error: {result.stderr}" + assert result.exit_code == 0, f"Command should succeed, got error: {result.stderr}" assert result.stdout, "Command should produce output" assert isinstance(result.duration_ms, int), "Duration should be measured" assert result.duration_ms > 0, "Duration should be positive" @@ -219,9 +209,7 @@ async def test_file_explorer_behavior(test_file_setup): assert list_result.total_files >= 1, "Should find at least one file" # Log what's found for debugging - logger.info( - f"Found {list_result.total_dirs} directories and {list_result.total_files} files" - ) + logger.info(f"Found {list_result.total_dirs} directories and {list_result.total_files} files") # Test 2: Test reading a specific file from the test directory # We know from fixture setup that dir1/file1.txt exists @@ -240,16 +228,14 @@ async def test_file_explorer_behavior(test_file_setup): assert grep_result.files_searched > 0, "Should search multiple files" # At least one match should be from our files - assert any( - "file" in match.line for match in grep_result.matches - ), "Should find 'file' string in matches" + assert any("file" in match.line for match in grep_result.matches), ( + "Should find 'file' string in matches" + ) # Test 4: Case sensitivity behavior # Our test_file_setup fixture creates a file with UPPERCASE text for these tests case_sensitive = await file_explorer.grep_files("UPPERCASE", "", True, None, True) - case_insensitive = await file_explorer.grep_files( - "uppercase", "", True, None, False - ) + case_insensitive = await file_explorer.grep_files("uppercase", "", True, None, False) # Verify case sensitivity behavior assert case_sensitive.total_matches > 0, "Should find case-sensitive matches" diff --git a/tests/unit/test_curl_dependency_reproduction.py b/tests/unit/test_curl_dependency_reproduction.py index 565e173..22ad344 100644 --- a/tests/unit/test_curl_dependency_reproduction.py +++ b/tests/unit/test_curl_dependency_reproduction.py @@ -195,9 +195,9 @@ async def test_curl_dependency_cascading_failure_to_kubectl( ): # Test that API server check fails due to curl dependency api_available = await bundle_manager.check_api_server_available() - assert ( - api_available is False - ), "API server should be unavailable when curl is missing" + assert api_available is False, ( + "API server should be unavailable when curl is missing" + ) # This demonstrates the cascading failure: # 1. curl is missing -> check_api_server_available() returns False @@ -213,9 +213,9 @@ async def test_curl_dependency_cascading_failure_to_kubectl( # Verify this by checking that the diagnostic info shows the problem diagnostics = await bundle_manager.get_diagnostic_info() - assert ( - diagnostics["api_server_available"] is False - ), "Diagnostics should show API server as unavailable due to curl dependency" + assert diagnostics["api_server_available"] is False, ( + "Diagnostics should show API server as unavailable due to curl dependency" + ) @pytest.mark.asyncio @@ -276,9 +276,9 @@ async def mock_subprocess_immediate_failure(*args, **kwargs): # The curl dependency failure should occur immediately, before any timeout result = await bundle_manager.check_api_server_available() - assert ( - result is False - ), "Should fail immediately due to missing curl, before any timeout" + assert result is False, ( + "Should fail immediately due to missing curl, before any timeout" + ) @pytest.mark.asyncio @@ -345,9 +345,7 @@ async def mock_subprocess_env_specific(*args, **kwargs): ): result = await bundle_manager.check_api_server_available() - assert ( - result is False - ), f"Should fail in {env['name']} environment without curl" + assert result is False, f"Should fail in {env['name']} environment without curl" @pytest.mark.asyncio diff --git a/tests/unit/test_files.py b/tests/unit/test_files.py index eac140a..96968c6 100644 --- a/tests/unit/test_files.py +++ b/tests/unit/test_files.py @@ -122,9 +122,7 @@ def test_grep_files_args_validation(): assert args_defaults.max_files == 10 # Default value # Test new parameters with custom values - args_custom = GrepFilesArgs( - pattern="test", path="dir1", max_results_per_file=3, max_files=5 - ) + args_custom = GrepFilesArgs(pattern="test", path="dir1", max_results_per_file=3, max_files=5) assert args_custom.max_results_per_file == 3 assert args_custom.max_files == 5 @@ -180,13 +178,9 @@ async def test_file_explorer_list_files(): result = await explorer.list_files("cluster-resources", True) # Verify behavior expectations for recursive listing - assert ( - result.path == "cluster-resources" - ), "Path should match requested directory" + assert result.path == "cluster-resources", "Path should match requested directory" assert result.recursive is True, "Recursive flag should be preserved" - assert ( - result.total_files >= 1 - ), "Should find at least 1 file in cluster-resources" + assert result.total_files >= 1, "Should find at least 1 file in cluster-resources" # Test 3: Verify result structure is correct (behavior contracts) for entry in result.entries: @@ -273,37 +267,27 @@ async def test_file_explorer_read_file(): result = await explorer.read_file("cluster-resources/pods/kube-system.json") # Verify behavior expectations - assert isinstance( - result, FileContentResult - ), "Result should be a FileContentResult" - assert ( - result.path == "cluster-resources/pods/kube-system.json" - ), "Path should be preserved in result" - assert ( - "test-pod" in result.content - ), "Content should match expected JSON content" + assert isinstance(result, FileContentResult), "Result should be a FileContentResult" + assert result.path == "cluster-resources/pods/kube-system.json", ( + "Path should be preserved in result" + ) + assert "test-pod" in result.content, "Content should match expected JSON content" assert result.binary is False, "JSON file should not be marked as binary" assert result.total_lines > 0, "Line count should be available" # Test 2: Reading a line range from the same file - result = await explorer.read_file( - "cluster-resources/pods/kube-system.json", 1, 3 - ) + result = await explorer.read_file("cluster-resources/pods/kube-system.json", 1, 3) # Verify behavior expectations for line ranges assert result.start_line == 1, "Start line should match requested value" assert result.end_line >= 1, "End line should be at least start line" - assert ( - len(result.content.split("\n")) <= 4 - ), "Should have limited lines based on range" + assert len(result.content.split("\n")) <= 4, "Should have limited lines based on range" # Test 3: Reading binary file (from the with_binaries structure) result = await explorer.read_file("binaries/fake_binary") # Verify behavior expectations for binary files - assert ( - result.path == "binaries/fake_binary" - ), "Path should be preserved in result" + assert result.path == "binaries/fake_binary", "Path should be preserved in result" assert result.binary is True, "Binary file should be marked as binary" @@ -369,9 +353,7 @@ async def test_file_explorer_grep_files(): test_dir.mkdir(exist_ok=True) # Create files with specific content for pattern matching - (test_dir / "case_test.txt").write_text( - "This contains UPPERCASE and lowercase text\n" - ) + (test_dir / "case_test.txt").write_text("This contains UPPERCASE and lowercase text\n") (test_dir / "pattern_test.txt").write_text( "This is a test file with patterns\nAnother line with test word\n" ) @@ -427,12 +409,8 @@ async def test_file_explorer_grep_files(): # Verify behavior expectations for case sensitivity assert case_sensitive.total_matches >= 1, "Should find exact case matches" - assert ( - case_insensitive.total_matches >= 1 - ), "Should find case-insensitive matches" - assert ( - case_insensitive.case_sensitive is False - ), "Should preserve case sensitivity flag" + assert case_insensitive.total_matches >= 1, "Should find case-insensitive matches" + assert case_insensitive.case_sensitive is False, "Should preserve case sensitivity flag" @pytest.mark.asyncio @@ -492,9 +470,7 @@ async def test_file_explorer_grep_files_with_kubeconfig(): # There should be matches in our reference file ref_file_matches = [m for m in result.matches if "reference.txt" in m.path] - assert ( - len(ref_file_matches) >= 3 - ), "Should find multiple matches in reference.txt" + assert len(ref_file_matches) >= 3, "Should find multiple matches in reference.txt" @pytest.mark.asyncio @@ -563,15 +539,11 @@ def test_file_explorer_is_binary(): # Test 1: Text file should not be marked as binary (JSON file from real structure) text_file_path = bundle_structure["kube_system_pods"] - assert not explorer._is_binary( - text_file_path - ), "JSON file should not be detected as binary" + assert not explorer._is_binary(text_file_path), "JSON file should not be detected as binary" # Test 2: Binary file should be marked as binary (real binary file) binary_file_path = bundle_structure["fake_binary"] - assert explorer._is_binary( - binary_file_path - ), "Binary file should be detected as binary" + assert explorer._is_binary(binary_file_path), "Binary file should be detected as binary" def test_file_explorer_normalize_path(): @@ -604,21 +576,21 @@ def test_file_explorer_normalize_path(): # Test 1: Normalizing a relative path normalized = explorer._normalize_path("cluster-resources") - assert ( - normalized == bundle_path / "cluster-resources" - ), "Relative path should be resolved to absolute path" + assert normalized == bundle_path / "cluster-resources", ( + "Relative path should be resolved to absolute path" + ) # Test 2: Normalizing a path with leading slashes normalized = explorer._normalize_path("/cluster-resources") - assert ( - normalized == bundle_path / "cluster-resources" - ), "Leading slashes should be handled properly" + assert normalized == bundle_path / "cluster-resources", ( + "Leading slashes should be handled properly" + ) # Test 3: Normalizing a nested path normalized = explorer._normalize_path("cluster-resources/pods") - assert ( - normalized == bundle_path / "cluster-resources" / "pods" - ), "Nested paths should be resolved correctly" + assert normalized == bundle_path / "cluster-resources" / "pods", ( + "Nested paths should be resolved correctly" + ) # Test 4: Security check - block directory traversal attempts with pytest.raises(InvalidPathError): diff --git a/tests/unit/test_files_parametrized.py b/tests/unit/test_files_parametrized.py index 5e2ed8e..ebeb305 100644 --- a/tests/unit/test_files_parametrized.py +++ b/tests/unit/test_files_parametrized.py @@ -121,9 +121,7 @@ def test_list_files_args_validation_parametrized(path, recursive, expected_valid "invalid-negative-end", ], ) -def test_read_file_args_validation_parametrized( - path, start_line, end_line, expected_valid -): +def test_read_file_args_validation_parametrized(path, start_line, end_line, expected_valid): """ Test ReadFileArgs validation with parameterized test cases. diff --git a/tests/unit/test_fixes_integration.py b/tests/unit/test_fixes_integration.py index bb6feaf..94f4143 100644 --- a/tests/unit/test_fixes_integration.py +++ b/tests/unit/test_fixes_integration.py @@ -23,9 +23,7 @@ async def test_transport_cleanup_fix_integration(): works correctly with subprocess_utils. """ if sys.version_info < (3, 13): - pytest.skip( - "This test is specifically for Python 3.13+ transport fix verification" - ) + pytest.skip("This test is specifically for Python 3.13+ transport fix verification") from mcp_server_troubleshoot.subprocess_utils import subprocess_exec_with_cleanup @@ -34,10 +32,7 @@ async def test_transport_cleanup_fix_integration(): def warning_capture(message, category, filename, lineno, file=None, line=None): msg_str = str(message) - if any( - keyword in msg_str.lower() - for keyword in ["transport", "_closing", "unclosed"] - ): + if any(keyword in msg_str.lower() for keyword in ["transport", "_closing", "unclosed"]): transport_issues.append(f"{category.__name__}: {msg_str}") original_showwarning = warnings.showwarning @@ -63,9 +58,7 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): f"The Python 3.13 transport cleanup fix may not be working correctly." ) - print( - "✅ Python 3.13 transport cleanup fix verified: No transport issues detected" - ) + print("✅ Python 3.13 transport cleanup fix verified: No transport issues detected") finally: warnings.showwarning = original_showwarning @@ -90,18 +83,12 @@ async def test_socket_port_checking_fix_integration(): try: # This should work without any netstat dependency result = bundle_manager._check_port_listening_python(port) - assert isinstance( - result, bool - ), f"Port check should return boolean for port {port}" - print( - f"✅ Port {port} check successful: {result} (socket-based, no netstat required)" - ) + assert isinstance(result, bool), f"Port check should return boolean for port {port}" + print(f"✅ Port {port} check successful: {result} (socket-based, no netstat required)") except Exception as e: pytest.fail(f"Socket-based port checking failed for port {port}: {e}") - print( - "✅ Socket-based port checking fix verified: All ports checked without netstat" - ) + print("✅ Socket-based port checking fix verified: All ports checked without netstat") @pytest.mark.asyncio @@ -126,15 +113,11 @@ async def test_netstat_replaced_in_diagnostic_info(): diagnostic_info = await bundle_manager.get_diagnostic_info() # Verify we got diagnostic information - assert isinstance( - diagnostic_info, dict - ), "Diagnostic info should be a dictionary" + assert isinstance(diagnostic_info, dict), "Diagnostic info should be a dictionary" # Look for evidence that socket-based checking was used port_checked_keys = [ - key - for key in diagnostic_info.keys() - if "port_" in key and "_checked" in key + key for key in diagnostic_info.keys() if "port_" in key and "_checked" in key ] # Note: Port checking only happens when sbctl is available @@ -147,19 +130,13 @@ async def test_netstat_replaced_in_diagnostic_info(): assert len(port_checked_keys) > 0, "Should have checked at least one port" # Verify no netstat-related errors - netstat_error_keys = [ - key for key in diagnostic_info.keys() if "netstat" in key.lower() - ] + netstat_error_keys = [key for key in diagnostic_info.keys() if "netstat" in key.lower()] if netstat_error_keys: # If there are netstat-related keys, they should be from old code, not our new code - print( - f"Found netstat-related keys (may be from old code): {netstat_error_keys}" - ) + print(f"Found netstat-related keys (may be from old code): {netstat_error_keys}") # Look for socket-based port checking evidence - socket_evidence = [ - key for key in diagnostic_info.keys() if "socket" in key.lower() - ] + socket_evidence = [key for key in diagnostic_info.keys() if "socket" in key.lower()] if socket_evidence: print(f"✅ Found evidence of socket-based checking: {socket_evidence}") @@ -187,10 +164,7 @@ async def test_both_fixes_work_together(): def warning_capture(message, category, filename, lineno, file=None, line=None): msg_str = str(message) - if any( - keyword in msg_str.lower() - for keyword in ["transport", "_closing", "unclosed"] - ): + if any(keyword in msg_str.lower() for keyword in ["transport", "_closing", "unclosed"]): transport_issues.append(f"{category.__name__}: {msg_str}") original_showwarning = warnings.showwarning @@ -215,9 +189,7 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): test_ports = [8080, 9090, 3000] for port in test_ports: port_status = bundle_manager._check_port_listening_python(port) - assert isinstance( - port_status, bool - ), f"Port {port} check should return boolean" + assert isinstance(port_status, bool), f"Port {port} check should return boolean" # Test full diagnostic info (integration of both fixes) diagnostic_info = await bundle_manager.get_diagnostic_info() @@ -229,14 +201,10 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): await asyncio.sleep(0.1) # Verify both fixes work - assert ( - len(transport_issues) == 0 - ), f"No transport issues should occur: {transport_issues}" + assert len(transport_issues) == 0, f"No transport issues should occur: {transport_issues}" port_checked_keys = [ - key - for key in diagnostic_info.keys() - if "port_" in key and "_checked" in key + key for key in diagnostic_info.keys() if "port_" in key and "_checked" in key ] # Note: Port checking only happens when sbctl is available @@ -246,9 +214,7 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): print("✅ Socket-based port checking is ready when sbctl is present") else: print(f"✅ Port checking worked: {port_checked_keys}") - assert ( - len(port_checked_keys) > 0 - ), "Port checking should work without netstat" + assert len(port_checked_keys) > 0, "Port checking should work without netstat" print("✅ Both fixes work correctly together:") print(" - Python 3.13 transport cleanup: No transport issues") @@ -271,15 +237,9 @@ def test_fixes_are_properly_implemented(): source = inspect.getsource(subprocess_utils) # Verify Python 3.13 specific code is present - assert ( - "sys.version_info >= (3, 13)" in source - ), "Should have Python 3.13 version check" - assert ( - "_safe_transport_cleanup" in source - ), "Should have safe transport cleanup function" - assert ( - "_safe_transport_wait_close" in source - ), "Should have safe transport wait function" + assert "sys.version_info >= (3, 13)" in source, "Should have Python 3.13 version check" + assert "_safe_transport_cleanup" in source, "Should have safe transport cleanup function" + assert "_safe_transport_wait_close" in source, "Should have safe transport wait function" # Check bundle.py has socket-based port checking from mcp_server_troubleshoot import bundle @@ -288,12 +248,12 @@ def test_fixes_are_properly_implemented(): # Verify socket import and usage assert "import socket" in bundle_source, "Should import socket module" - assert ( - "_check_port_listening_python" in bundle_source - ), "Should have Python port checking method" - assert ( - "socket.socket(socket.AF_INET, socket.SOCK_STREAM)" in bundle_source - ), "Should use socket for port checking" + assert "_check_port_listening_python" in bundle_source, ( + "Should have Python port checking method" + ) + assert "socket.socket(socket.AF_INET, socket.SOCK_STREAM)" in bundle_source, ( + "Should use socket for port checking" + ) # Verify netstat is no longer used in the port checking code # (It might still be mentioned in comments or documentation) @@ -302,9 +262,9 @@ def test_fixes_are_properly_implemented(): if "netstat" in line.lower() and "subprocess_exec_with_cleanup(" in line: netstat_usage_lines.append(f"Line {line_num}: {line.strip()}") - assert ( - len(netstat_usage_lines) == 0 - ), f"Found active netstat usage that should be replaced: {netstat_usage_lines}" + assert len(netstat_usage_lines) == 0, ( + f"Found active netstat usage that should be replaced: {netstat_usage_lines}" + ) print("✅ Both fixes are properly implemented in the codebase:") print(" - subprocess_utils: Python 3.13 transport handling") diff --git a/tests/unit/test_grep_fix.py b/tests/unit/test_grep_fix.py index 8a89adf..ccdba76 100644 --- a/tests/unit/test_grep_fix.py +++ b/tests/unit/test_grep_fix.py @@ -30,9 +30,7 @@ async def test_grep_files_with_kubeconfig(tmp_path: Path): # Create a kubeconfig file kubeconfig_path = bundle_dir / "kubeconfig" - kubeconfig_path.write_text( - "apiVersion: v1\nkind: Config\nclusters:\n- name: test-cluster\n" - ) + kubeconfig_path.write_text("apiVersion: v1\nkind: Config\nclusters:\n- name: test-cluster\n") print(f"Created kubeconfig file at: {kubeconfig_path}") # Create a directory with a few test files @@ -41,9 +39,7 @@ async def test_grep_files_with_kubeconfig(tmp_path: Path): # Create a file with 'kubeconfig' in its contents ref_file = test_dir / "config-reference.txt" - ref_file.write_text( - "This file refers to a kubeconfig file.\nThe kubeconfig path is important." - ) + ref_file.write_text("This file refers to a kubeconfig file.\nThe kubeconfig path is important.") print(f"Created reference file at: {ref_file}") # Create a nested kubeconfig file @@ -69,9 +65,7 @@ async def test_grep_files_with_kubeconfig(tmp_path: Path): explorer = FileExplorer(bundle_manager) # Test searching for "kubeconfig" (case insensitive) - result = await explorer.grep_files( - "kubeconfig", "", recursive=True, case_sensitive=False - ) + result = await explorer.grep_files("kubeconfig", "", recursive=True, case_sensitive=False) # Print the results print("\nSearch results for 'kubeconfig':") diff --git a/tests/unit/test_kubectl.py b/tests/unit/test_kubectl.py index 3ec1138..c1438f0 100644 --- a/tests/unit/test_kubectl.py +++ b/tests/unit/test_kubectl.py @@ -144,9 +144,7 @@ async def test_kubectl_executor_execute_success(): # Verify the result assert result == mock_result - executor._run_kubectl_command.assert_awaited_once_with( - "get pods", bundle, 30, False - ) + executor._run_kubectl_command.assert_awaited_once_with("get pods", bundle, 30, False) @pytest.mark.asyncio @@ -172,9 +170,7 @@ async def test_kubectl_executor_run_kubectl_command(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch( - "asyncio.create_subprocess_exec", return_value=mock_process - ) as mock_exec: + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: # Execute a command result = await executor._run_kubectl_command("get pods", bundle, 30, True) @@ -225,9 +221,7 @@ async def test_kubectl_executor_run_kubectl_command_no_json(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch( - "asyncio.create_subprocess_exec", return_value=mock_process - ) as mock_exec: + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: # Execute a command result = await executor._run_kubectl_command("get pods", bundle, 30, False) @@ -269,21 +263,15 @@ async def test_kubectl_executor_run_kubectl_command_explicit_format(): # Mock subprocess mock_process = AsyncMock() mock_process.returncode = 0 - mock_process.communicate = AsyncMock( - return_value=(b"name: pod1\nstatus: Running", b"") - ) + mock_process.communicate = AsyncMock(return_value=(b"name: pod1\nstatus: Running", b"")) # Create the executor executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch( - "asyncio.create_subprocess_exec", return_value=mock_process - ) as mock_exec: + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: # Execute a command with explicit format - result = await executor._run_kubectl_command( - "get pods -o yaml", bundle, 30, True - ) + result = await executor._run_kubectl_command("get pods -o yaml", bundle, 30, True) # Verify the result assert result.command == "get pods -o yaml" @@ -321,9 +309,7 @@ async def test_kubectl_executor_run_kubectl_command_error(): # Mock subprocess mock_process = AsyncMock() mock_process.returncode = 1 - mock_process.communicate = AsyncMock( - return_value=(b"", b'Error: resource "pods" not found') - ) + mock_process.communicate = AsyncMock(return_value=(b"", b'Error: resource "pods" not found')) # Create the executor executor = KubectlExecutor(bundle_manager) @@ -379,9 +365,7 @@ async def mock_subprocess_timeout(*args, **kwargs): ): # Execute a command with a short timeout with pytest.raises(KubectlError) as excinfo: - await executor._run_kubectl_command( - "get pods", bundle, 0.1, True - ) # 0.1 second timeout + await executor._run_kubectl_command("get pods", bundle, 0.1, True) # 0.1 second timeout # Verify the error assert "kubectl command timed out" in str(excinfo.value) @@ -446,9 +430,7 @@ async def test_kubectl_default_cli_format(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch( - "asyncio.create_subprocess_exec", return_value=mock_process - ) as mock_exec: + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: # Execute with default json_output=False result = await executor._run_kubectl_command("get pods", bundle, 30, False) @@ -489,9 +471,7 @@ async def test_kubectl_explicit_json_request(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch( - "asyncio.create_subprocess_exec", return_value=mock_process - ) as mock_exec: + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: # Execute with explicit json_output=True result = await executor._run_kubectl_command("get pods", bundle, 30, True) @@ -532,13 +512,9 @@ async def test_kubectl_user_format_preserved(): executor = KubectlExecutor(bundle_manager) # Mock create_subprocess_exec - with patch( - "asyncio.create_subprocess_exec", return_value=mock_process - ) as mock_exec: + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: # Execute with user-specified YAML format - result = await executor._run_kubectl_command( - "get pods -o yaml", bundle, 30, False - ) + result = await executor._run_kubectl_command("get pods -o yaml", bundle, 30, False) # Verify user format is preserved assert result.command == "get pods -o yaml" # No modification @@ -584,15 +560,11 @@ async def test_kubectl_executor_defaults_to_table_format(): result = await executor.execute("get pods") # Verify the result is NOT JSON - assert ( - result.is_json is False - ), "Default kubectl execution should NOT return JSON format" + assert result.is_json is False, "Default kubectl execution should NOT return JSON format" assert result == mock_result # Verify _run_kubectl_command was called with json_output=False (the new default) - executor._run_kubectl_command.assert_awaited_once_with( - "get pods", bundle, 30, False - ) + executor._run_kubectl_command.assert_awaited_once_with("get pods", bundle, 30, False) def test_compact_json_formatting(): diff --git a/tests/unit/test_kubectl_parametrized.py b/tests/unit/test_kubectl_parametrized.py index 6851dde..feb4bf5 100644 --- a/tests/unit/test_kubectl_parametrized.py +++ b/tests/unit/test_kubectl_parametrized.py @@ -78,18 +78,14 @@ def test_kubectl_command_args_validation_parametrized( """ if expected_valid: # Should succeed - args = KubectlCommandArgs( - command=command, timeout=timeout, json_output=json_output - ) + args = KubectlCommandArgs(command=command, timeout=timeout, json_output=json_output) assert args.command == command assert args.timeout == timeout assert args.json_output == json_output else: # Should raise ValidationError with pytest.raises(ValidationError): - KubectlCommandArgs( - command=command, timeout=timeout, json_output=json_output - ) + KubectlCommandArgs(command=command, timeout=timeout, json_output=json_output) @pytest.mark.asyncio @@ -124,9 +120,7 @@ def test_kubectl_command_args_validation_parametrized( "version", ], ) -async def test_kubectl_command_execution_parameters( - command, expected_args, add_json, test_factory -): +async def test_kubectl_command_execution_parameters(command, expected_args, add_json, test_factory): """ Test that the kubectl executor handles different command formats correctly. @@ -157,9 +151,7 @@ async def test_kubectl_command_execution_parameters( expected_args.extend(["-o", "json"]) # Mock the create_subprocess_exec function - with patch( - "asyncio.create_subprocess_exec", return_value=mock_process - ) as mock_exec: + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: # Execute the command result = await executor._run_kubectl_command(command, bundle, 30, True) @@ -379,9 +371,7 @@ async def test_kubectl_response_parsing(test_assertions, test_factory): assert is_json == case["expected_is_json"], f"Case {i}: JSON detection failed" # Assert the output was processed to the right type - assert isinstance( - processed, case["expected_type"] - ), f"Case {i}: Wrong output type" + assert isinstance(processed, case["expected_type"]), f"Case {i}: Wrong output type" # For JSON outputs, verify structure if case["expected_is_json"]: diff --git a/tests/unit/test_lifecycle.py b/tests/unit/test_lifecycle.py index 6325a26..a202059 100644 --- a/tests/unit/test_lifecycle.py +++ b/tests/unit/test_lifecycle.py @@ -42,9 +42,7 @@ async def test_periodic_bundle_cleanup(): mock_bundle_manager = AsyncMock() # Create a task with a short interval - task = asyncio.create_task( - periodic_bundle_cleanup(mock_bundle_manager, interval=0.1) - ) + task = asyncio.create_task(periodic_bundle_cleanup(mock_bundle_manager, interval=0.1)) # Let it run for a short time await asyncio.sleep(0.3) @@ -68,9 +66,7 @@ async def test_lifecycle_context_normal_exit(): mock_server.use_stdio = True # Set environment variables for the test - with patch.dict( - os.environ, {"ENABLE_PERIODIC_CLEANUP": "true", "CLEANUP_INTERVAL": "60"} - ): + with patch.dict(os.environ, {"ENABLE_PERIODIC_CLEANUP": "true", "CLEANUP_INTERVAL": "60"}): # Enter the context manager async with app_lifespan(mock_server) as context: # Verify resources were initialized diff --git a/tests/unit/test_list_bundles.py b/tests/unit/test_list_bundles.py index 46e344c..823c97e 100644 --- a/tests/unit/test_list_bundles.py +++ b/tests/unit/test_list_bundles.py @@ -79,9 +79,7 @@ async def test_list_available_bundles_valid_bundle(temp_bundle_dir, mock_valid_b @pytest.mark.asyncio -async def test_list_available_bundles_invalid_bundle( - temp_bundle_dir, mock_invalid_bundle -): +async def test_list_available_bundles_invalid_bundle(temp_bundle_dir, mock_invalid_bundle): """Test listing bundles with an invalid bundle.""" bundle_manager = BundleManager(temp_bundle_dir) @@ -156,9 +154,7 @@ async def test_bundle_validity_checker( assert message is not None # Non-existing file - valid, message = bundle_manager._check_bundle_validity( - Path("/non/existing/file.tar.gz") - ) + valid, message = bundle_manager._check_bundle_validity(Path("/non/existing/file.tar.gz")) assert valid is False assert message is not None @@ -191,9 +187,7 @@ async def test_relative_path_initialization(temp_bundle_dir, mock_valid_bundle): # Instead of monkeypatching internal methods, we'll mock at a higher level # This focuses on the behavior (initializing a bundle) rather than implementation - with patch.object( - bundle_manager, "_initialize_with_sbctl", autospec=False - ) as mock_init: + with patch.object(bundle_manager, "_initialize_with_sbctl", autospec=False) as mock_init: # Set up the mock to create the kubeconfig file and return its path async def side_effect(bundle_path, output_dir): logger.info(f"Creating mock kubeconfig in {output_dir}") @@ -242,9 +236,7 @@ async def test_bundle_path_resolution_behavior(temp_bundle_dir, mock_valid_bundl bundle_manager = BundleManager(temp_bundle_dir) # Create patch for _initialize_with_sbctl to avoid actual initialization - with patch.object( - bundle_manager, "_initialize_with_sbctl", autospec=False - ) as mock_init: + with patch.object(bundle_manager, "_initialize_with_sbctl", autospec=False) as mock_init: # Set up the mock to return a valid kubeconfig path async def side_effect(bundle_path, output_dir): os.makedirs(output_dir, exist_ok=True) diff --git a/tests/unit/test_netstat_dependency.py b/tests/unit/test_netstat_dependency.py index 2a5b052..8048f40 100644 --- a/tests/unit/test_netstat_dependency.py +++ b/tests/unit/test_netstat_dependency.py @@ -35,15 +35,11 @@ async def test_bundle_api_check_without_netstat(): clean_path_parts = [] for path_part in original_path.split(":"): # Skip paths that typically contain netstat - if not any( - common in path_part.lower() for common in ["/bin", "/sbin", "/usr"] - ): + if not any(common in path_part.lower() for common in ["/bin", "/sbin", "/usr"]): clean_path_parts.append(path_part) # Set a very minimal PATH that won't have netstat - os.environ["PATH"] = ( - ":".join(clean_path_parts) if clean_path_parts else "/nonexistent" - ) + os.environ["PATH"] = ":".join(clean_path_parts) if clean_path_parts else "/nonexistent" # Now try to execute netstat - this should fail with FileNotFoundError try: @@ -211,9 +207,7 @@ def check_port_listening_python(port: int) -> bool: try: # Test on a port that's likely free - port_free = check_port_listening_python( - 0 - ) # Port 0 is special - always available + port_free = check_port_listening_python(0) # Port 0 is special - always available # Test on a port that might be in use port_in_use = check_port_listening_python(22) # SSH port often in use @@ -228,9 +222,7 @@ def check_port_listening_python(port: int) -> bool: # For TDD: The current (netstat) approach should fail, # and the replacement (Python socket) approach should work if netstat_failed and python_socket_works: - print( - "✅ TDD SUCCESS: Demonstrated the netstat dependency problem and solution!" - ) + print("✅ TDD SUCCESS: Demonstrated the netstat dependency problem and solution!") print(f"❌ Current netstat approach failed: {netstat_error}") print("✅ Python socket replacement works correctly") print("✅ Test confirms both the problem and the solution work as expected") diff --git a/tests/unit/test_ps_pkill_dependency.py b/tests/unit/test_ps_pkill_dependency.py index 0abbfdf..61b8961 100644 --- a/tests/unit/test_ps_pkill_dependency.py +++ b/tests/unit/test_ps_pkill_dependency.py @@ -53,9 +53,7 @@ async def test_bundle_cleanup_without_ps_pkill(): pkill_failed = False pkill_error = None try: - pkill_result = subprocess.run( - ["pkill", "-f", "sbctl"], capture_output=True, text=True - ) + pkill_result = subprocess.run(["pkill", "-f", "sbctl"], capture_output=True, text=True) pytest.fail( f"❌ TDD PROBLEM: pkill command was found and executed successfully!\n" f"Return code: {pkill_result.returncode}\n" @@ -133,15 +131,11 @@ def terminate_processes_by_name(process_name: str) -> int: python_processes = find_processes_by_name("python") # Should find at least this test process - assert ( - len(python_processes) >= 1 - ), "Should find at least the current python process" + assert len(python_processes) >= 1, "Should find at least the current python process" # Verify the returned objects are psutil Process instances for proc in python_processes: - assert isinstance( - proc, psutil.Process - ), "Should return psutil Process objects" + assert isinstance(proc, psutil.Process), "Should return psutil Process objects" # Test that we can access process information current_process = psutil.Process() # Current process @@ -153,9 +147,7 @@ def terminate_processes_by_name(process_name: str) -> int: # Test termination function (but don't actually terminate anything) # Just verify the function structure works without causing harm - fake_process_count = terminate_processes_by_name( - "nonexistent_process_name_12345" - ) + fake_process_count = terminate_processes_by_name("nonexistent_process_name_12345") assert fake_process_count == 0, "Should return 0 for non-existent processes" # SUCCESS: The psutil-based implementation works correctly @@ -259,9 +251,7 @@ def test_psutil_availability(): # Test process filtering functionality python_procs = [ - p - for p in psutil.process_iter(["name"]) - if "python" in p.info["name"].lower() + p for p in psutil.process_iter(["name"]) if "python" in p.info["name"].lower() ] assert len(python_procs) >= 1, "Should find at least one python process" @@ -271,9 +261,7 @@ def test_psutil_availability(): print(f"✅ Found {len(python_procs)} python processes") except ImportError: - pytest.fail( - "❌ psutil is not available! Add psutil to pyproject.toml dependencies." - ) + pytest.fail("❌ psutil is not available! Add psutil to pyproject.toml dependencies.") except Exception as e: pytest.fail( f"❌ psutil functionality test failed: {str(e)}\nError type: {type(e).__name__}" diff --git a/tests/unit/test_python313_transport_issue.py b/tests/unit/test_python313_transport_issue.py index 84ddba2..49564ed 100644 --- a/tests/unit/test_python313_transport_issue.py +++ b/tests/unit/test_python313_transport_issue.py @@ -41,9 +41,7 @@ async def test_subprocess_transport_cleanup_triggers_error(): # Look for Python 3.13 compatibility checks has_python313_check = "3.13" in source or "version_info" in source has_transport_cleanup = "_closing" in source or "transport" in source.lower() - has_pipe_transport_fix = ( - "UnixReadPipeTransport" in source or "pipe_transport" in source - ) + has_pipe_transport_fix = "UnixReadPipeTransport" in source or "pipe_transport" in source # Current subprocess_utils should NOT have these fixes yet (for TDD) if has_python313_check and has_transport_cleanup and has_pipe_transport_fix: @@ -77,9 +75,7 @@ def warning_capture(message, category, filename, lineno, file=None, line=None): returncode, stdout, stderr = await subprocess_exec_with_cleanup( "echo", f"transport_stress_test_{i}", timeout=5.0 ) - assert ( - returncode == 0 - ), f"subprocess_exec_with_cleanup failed at iteration {i}" + assert returncode == 0, f"subprocess_exec_with_cleanup failed at iteration {i}" # Force garbage collection aggressively for _ in range(15): @@ -149,9 +145,7 @@ def error_capture(message, category, filename, lineno, file=None, line=None): returncode, stdout, stderr = await subprocess_exec_with_cleanup( "echo", f"subprocess_utils_test_{i}", timeout=5.0 ) - assert ( - returncode == 0 - ), f"subprocess_exec_with_cleanup failed: {stderr.decode()}" + assert returncode == 0, f"subprocess_exec_with_cleanup failed: {stderr.decode()}" # Force garbage collection to trigger transport cleanup for _ in range(10): @@ -270,9 +264,7 @@ def patched_collect(): print("✅ Transport cleanup fixes appear to be handling the issue") # Test passes - even if some transports are live, no errors occurred else: - print( - "✅ TDD SUCCESS: No _closing attribute errors and no leaked transports!" - ) + print("✅ TDD SUCCESS: No _closing attribute errors and no leaked transports!") print("✅ Python 3.13 transport cleanup fixes are working correctly") print("✅ Transport lifecycle is properly managed") # Test passes - perfect cleanup with no issues diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index 48f9026..e0a399c 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -83,15 +83,11 @@ async def test_initialize_bundle_tool(tmp_path: Path) -> None: patch.object( bundle_manager, "_check_sbctl_available", new_callable=AsyncMock ) as mock_sbctl, - patch.object( - bundle_manager, "initialize_bundle", new_callable=AsyncMock - ) as mock_init, + patch.object(bundle_manager, "initialize_bundle", new_callable=AsyncMock) as mock_init, patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object( - bundle_manager, "get_diagnostic_info", new_callable=AsyncMock - ) as mock_diag, + patch.object(bundle_manager, "get_diagnostic_info", new_callable=AsyncMock) as mock_diag, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, ): # Set up mocks for external dependencies only @@ -104,9 +100,7 @@ async def test_initialize_bundle_tool(tmp_path: Path) -> None: # Create InitializeBundleArgs instance from mcp_server_troubleshoot.bundle import InitializeBundleArgs - args = InitializeBundleArgs( - source=str(temp_source_file), force=False, verbosity="verbose" - ) + args = InitializeBundleArgs(source=str(temp_source_file), force=False, verbosity="verbose") # Call the tool function directly response = await initialize_bundle(args) @@ -170,15 +164,9 @@ async def test_kubectl_tool(tmp_path: Path) -> None: patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object( - kubectl_executor, "execute", new_callable=AsyncMock - ) as mock_execute, - patch( - "mcp_server_troubleshoot.server.get_bundle_manager" - ) as mock_get_manager, - patch( - "mcp_server_troubleshoot.server.get_kubectl_executor" - ) as mock_get_executor, + patch.object(kubectl_executor, "execute", new_callable=AsyncMock) as mock_execute, + patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, + patch("mcp_server_troubleshoot.server.get_kubectl_executor") as mock_get_executor, ): # Set up mocks for external dependencies only mock_api.return_value = True @@ -243,9 +231,7 @@ async def test_kubectl_tool_host_only_bundle(tmp_path: Path) -> None: with ( patch.object(bundle_manager, "get_active_bundle", return_value=mock_bundle), - patch( - "mcp_server_troubleshoot.server.get_bundle_manager" - ) as mock_get_manager, + patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, ): mock_get_manager.return_value = bundle_manager @@ -312,18 +298,14 @@ async def test_file_operations(tmp_path: Path) -> None: with ( patch.object(bundle_manager, "get_active_bundle", return_value=mock_bundle), - patch( - "mcp_server_troubleshoot.server.get_file_explorer" - ) as mock_get_explorer, + patch("mcp_server_troubleshoot.server.get_file_explorer") as mock_get_explorer, ): mock_get_explorer.return_value = file_explorer # 1. Test list_files with real files from mcp_server_troubleshoot.files import ListFilesArgs - list_args = ListFilesArgs( - path="test_data/dir1", recursive=False, verbosity="verbose" - ) + list_args = ListFilesArgs(path="test_data/dir1", recursive=False, verbosity="verbose") list_response = await list_files(list_args) # Verify the response contains real file information diff --git a/tests/unit/test_server_parametrized.py b/tests/unit/test_server_parametrized.py index b48ef5d..692e304 100644 --- a/tests/unit/test_server_parametrized.py +++ b/tests/unit/test_server_parametrized.py @@ -145,15 +145,11 @@ async def test_initialize_bundle_tool_parametrized( patch.object( bundle_manager, "_check_sbctl_available", new_callable=AsyncMock ) as mock_sbctl, - patch.object( - bundle_manager, "initialize_bundle", new_callable=AsyncMock - ) as mock_init, + patch.object(bundle_manager, "initialize_bundle", new_callable=AsyncMock) as mock_init, patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object( - bundle_manager, "get_diagnostic_info", new_callable=AsyncMock - ) as mock_diag, + patch.object(bundle_manager, "get_diagnostic_info", new_callable=AsyncMock) as mock_diag, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, ): # Set up mocks for external dependencies only @@ -166,9 +162,7 @@ async def test_initialize_bundle_tool_parametrized( # Create InitializeBundleArgs instance from mcp_server_troubleshoot.bundle import InitializeBundleArgs - args = InitializeBundleArgs( - source=str(temp_source_file), force=force, verbosity="verbose" - ) + args = InitializeBundleArgs(source=str(temp_source_file), force=force, verbosity="verbose") # Call the tool function response = await initialize_bundle(args) @@ -278,16 +272,10 @@ async def test_kubectl_tool_parametrized( patch.object( bundle_manager, "check_api_server_available", new_callable=AsyncMock ) as mock_api, - patch.object( - bundle_manager, "get_diagnostic_info", new_callable=AsyncMock - ) as mock_diag, - patch.object( - kubectl_executor, "execute", new_callable=AsyncMock - ) as mock_execute, + patch.object(bundle_manager, "get_diagnostic_info", new_callable=AsyncMock) as mock_diag, + patch.object(kubectl_executor, "execute", new_callable=AsyncMock) as mock_execute, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, - patch( - "mcp_server_troubleshoot.server.get_kubectl_executor" - ) as mock_get_executor, + patch("mcp_server_troubleshoot.server.get_kubectl_executor") as mock_get_executor, ): # Set up mocks mock_api.return_value = True @@ -655,22 +643,16 @@ async def test_file_operations_error_handling( ) # 1. Test list_files - list_args = ListFilesArgs( - path="test/path", recursive=False, verbosity="verbose" - ) + list_args = ListFilesArgs(path="test/path", recursive=False, verbosity="verbose") list_response = await list_files(list_args) - test_assertions.assert_api_response_valid( - list_response, "text", expected_strings - ) + test_assertions.assert_api_response_valid(list_response, "text", expected_strings) # 2. Test read_file read_args = ReadFileArgs( path="test/file.txt", start_line=0, end_line=None, verbosity="verbose" ) read_response = await read_file(read_args) - test_assertions.assert_api_response_valid( - read_response, "text", expected_strings - ) + test_assertions.assert_api_response_valid(read_response, "text", expected_strings) # 3. Test grep_files grep_args = GrepFilesArgs( @@ -685,9 +667,7 @@ async def test_file_operations_error_handling( verbosity="verbose", ) grep_response = await grep_files(grep_args) - test_assertions.assert_api_response_valid( - grep_response, "text", expected_strings - ) + test_assertions.assert_api_response_valid(grep_response, "text", expected_strings) # No manual cleanup needed - tmp_path handles it automatically @@ -787,9 +767,7 @@ class MockAvailableBundle: # Mock only the list_available_bundles method, keep rest of BundleManager real with ( - patch.object( - bundle_manager, "list_available_bundles", new_callable=AsyncMock - ) as mock_list, + patch.object(bundle_manager, "list_available_bundles", new_callable=AsyncMock) as mock_list, patch("mcp_server_troubleshoot.server.get_bundle_manager") as mock_get_manager, ): mock_list.return_value = bundles @@ -798,9 +776,7 @@ class MockAvailableBundle: # Create ListAvailableBundlesArgs instance from mcp_server_troubleshoot.bundle import ListAvailableBundlesArgs - args = ListAvailableBundlesArgs( - include_invalid=include_invalid, verbosity="verbose" - ) + args = ListAvailableBundlesArgs(include_invalid=include_invalid, verbosity="verbose") # Call the tool function response = await list_available_bundles(args) diff --git a/tests/unit/test_size_limiter.py b/tests/unit/test_size_limiter.py index b835adc..c071b63 100644 --- a/tests/unit/test_size_limiter.py +++ b/tests/unit/test_size_limiter.py @@ -79,9 +79,7 @@ def test_size_limit_thresholds(token_count, limit, should_pass): (None, 25000), # Default when not set ], ) -def test_mcp_token_limit_environment_variable( - mock_environment, env_value, expected_limit -): +def test_mcp_token_limit_environment_variable(mock_environment, env_value, expected_limit): """ Test MCP_TOKEN_LIMIT environment variable configuration. """ @@ -100,9 +98,7 @@ def test_mcp_token_limit_environment_variable( (None, True), # Default when not set ], ) -def test_mcp_size_check_enabled_environment_variable( - mock_environment, env_value, expected_enabled -): +def test_mcp_size_check_enabled_environment_variable(mock_environment, env_value, expected_enabled): """ Test MCP_SIZE_CHECK_ENABLED environment variable configuration. """ diff --git a/tests/unit/test_transport_cleanup_reproduction.py b/tests/unit/test_transport_cleanup_reproduction.py index 301daea..058b7e5 100644 --- a/tests/unit/test_transport_cleanup_reproduction.py +++ b/tests/unit/test_transport_cleanup_reproduction.py @@ -186,9 +186,7 @@ async def test_subprocess_transport_cleanup_single_operation( @pytest.mark.asyncio -async def test_subprocess_transport_cleanup_multiple_operations( - clean_asyncio, transport_detector -): +async def test_subprocess_transport_cleanup_multiple_operations(clean_asyncio, transport_detector): """ Test transport cleanup with multiple rapid subprocess operations. @@ -218,9 +216,7 @@ async def test_subprocess_transport_cleanup_multiple_operations( @pytest.mark.asyncio -async def test_concurrent_subprocess_transport_cleanup( - clean_asyncio, transport_detector -): +async def test_concurrent_subprocess_transport_cleanup(clean_asyncio, transport_detector): """ Test transport cleanup with concurrent subprocess operations. @@ -230,9 +226,7 @@ async def test_concurrent_subprocess_transport_cleanup( # Create multiple concurrent subprocess operations tasks = [] for i in range(3): - task = asyncio.create_task( - create_subprocess_and_wait(["echo", f"concurrent_{i}"]) - ) + task = asyncio.create_task(create_subprocess_and_wait(["echo", f"concurrent_{i}"])) tasks.append(task) # Wait for all tasks to complete @@ -340,9 +334,7 @@ async def test_subprocess_pattern_like_curl(clean_asyncio, transport_detector): @pytest.mark.asyncio -async def test_transport_cleanup_with_process_termination( - clean_asyncio, transport_detector -): +async def test_transport_cleanup_with_process_termination(clean_asyncio, transport_detector): """ Test transport cleanup when processes are forcibly terminated. @@ -391,9 +383,7 @@ async def test_event_loop_with_many_transports(clean_asyncio, transport_detector # Create many subprocess operations to stress test transport cleanup tasks = [] for i in range(8): # Reasonable number to avoid overwhelming the system - task = asyncio.create_task( - create_subprocess_and_wait(["echo", f"stress_test_{i}"]) - ) + task = asyncio.create_task(create_subprocess_and_wait(["echo", f"stress_test_{i}"])) tasks.append(task) # Wait for all to complete @@ -493,9 +483,7 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): if captured_warnings: print(f"\nCaptured {len(captured_warnings)} warnings:") for w in captured_warnings: - print( - f" {w['category']}: {w['message']} ({w['filename']}:{w['lineno']})" - ) + print(f" {w['category']}: {w['message']} ({w['filename']}:{w['lineno']})") if transport_warnings: print(f"\nFound {len(transport_warnings)} transport-related warnings:") @@ -543,8 +531,7 @@ async def test_aggressive_subprocess_cleanup_stress(): def capture_warnings(message, category, filename, lineno, file=None, line=None): msg_str = str(message) if any( - keyword in msg_str.lower() - for keyword in ["transport", "unclosed", "_closing", "pipe"] + keyword in msg_str.lower() for keyword in ["transport", "unclosed", "_closing", "pipe"] ): captured_issues.append(f"{category.__name__}: {msg_str}") @@ -632,9 +619,7 @@ async def test_simulate_unix_read_pipe_transport_missing_closing_attribute(): def mock_transport_repr_error(): """Simulate the _closing attribute missing error during __repr__""" - raise AttributeError( - "'_UnixReadPipeTransport' object has no attribute '_closing'" - ) + raise AttributeError("'_UnixReadPipeTransport' object has no attribute '_closing'") try: # Remove warning filters to see all warnings diff --git a/tests/util/debug_mcp.py b/tests/util/debug_mcp.py index e261975..e1329f6 100755 --- a/tests/util/debug_mcp.py +++ b/tests/util/debug_mcp.py @@ -83,9 +83,7 @@ def read_stderr(): if response_line: try: response = json.loads(response_line.decode("utf-8")) - print( - f"Received JSON-RPC response: {json.dumps(response, indent=2)}" - ) + print(f"Received JSON-RPC response: {json.dumps(response, indent=2)}") except json.JSONDecodeError as e: print(f"Failed to decode response as JSON: {e}") else: