Simplified /workspace to map to a project scoped WORKSPACE_ROOT
This commit is contained in:
@@ -64,7 +64,3 @@ DEFAULT_THINKING_MODE_THINKDEEP=high
|
|||||||
# ERROR: Shows only errors
|
# ERROR: Shows only errors
|
||||||
LOG_LEVEL=DEBUG
|
LOG_LEVEL=DEBUG
|
||||||
|
|
||||||
# Optional: Project root override for file sandboxing
|
|
||||||
# If set, overrides the default sandbox directory
|
|
||||||
# Use with caution - this controls which files the server can access
|
|
||||||
# MCP_PROJECT_ROOT=/path/to/specific/project
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
Communication Simulator Test for Zen MCP Server
|
Communication Simulator Test for Zen MCP Server
|
||||||
|
|
||||||
@@ -13,20 +12,28 @@ Test Flow:
|
|||||||
4. Cleanup and report results
|
4. Cleanup and report results
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python communication_simulator_test.py [--verbose] [--keep-logs] [--tests TEST_NAME...] [--individual TEST_NAME] [--skip-docker]
|
python communication_simulator_test.py [--verbose] [--keep-logs] [--tests TEST_NAME...] [--individual TEST_NAME] [--rebuild]
|
||||||
|
|
||||||
--tests: Run specific tests only (space-separated)
|
--tests: Run specific tests only (space-separated)
|
||||||
--list-tests: List all available tests
|
--list-tests: List all available tests
|
||||||
--individual: Run a single test individually
|
--individual: Run a single test individually
|
||||||
--skip-docker: Skip Docker setup (assumes containers are already running)
|
--rebuild: Force rebuild Docker environment using setup-docker.sh
|
||||||
|
|
||||||
Available tests:
|
Available tests:
|
||||||
basic_conversation - Basic conversation flow with chat tool
|
basic_conversation - Basic conversation flow with chat tool
|
||||||
|
content_validation - Content validation and duplicate detection
|
||||||
per_tool_deduplication - File deduplication for individual tools
|
per_tool_deduplication - File deduplication for individual tools
|
||||||
cross_tool_continuation - Cross-tool conversation continuation scenarios
|
cross_tool_continuation - Cross-tool conversation continuation scenarios
|
||||||
content_validation - Content validation and duplicate detection
|
cross_tool_comprehensive - Comprehensive cross-tool integration testing
|
||||||
logs_validation - Docker logs validation
|
logs_validation - Docker logs validation
|
||||||
redis_validation - Redis conversation memory validation
|
redis_validation - Redis conversation memory validation
|
||||||
|
model_thinking_config - Model thinking configuration testing
|
||||||
|
o3_model_selection - O3 model selection and routing testing
|
||||||
|
ollama_custom_url - Ollama custom URL configuration testing
|
||||||
|
openrouter_fallback - OpenRouter fallback mechanism testing
|
||||||
|
openrouter_models - OpenRouter models availability testing
|
||||||
|
token_allocation_validation - Token allocation and limits validation
|
||||||
|
conversation_chain_validation - Conversation chain continuity validation
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Run all tests
|
# Run all tests
|
||||||
@@ -38,8 +45,8 @@ Examples:
|
|||||||
# Run a single test individually (with full Docker setup)
|
# Run a single test individually (with full Docker setup)
|
||||||
python communication_simulator_test.py --individual content_validation
|
python communication_simulator_test.py --individual content_validation
|
||||||
|
|
||||||
# Run a single test individually (assuming Docker is already running)
|
# Force rebuild Docker environment before running tests
|
||||||
python communication_simulator_test.py --individual content_validation --skip-docker
|
python communication_simulator_test.py --rebuild
|
||||||
|
|
||||||
# List available tests
|
# List available tests
|
||||||
python communication_simulator_test.py --list-tests
|
python communication_simulator_test.py --list-tests
|
||||||
@@ -52,16 +59,18 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class CommunicationSimulator:
|
class CommunicationSimulator:
|
||||||
"""Simulates real-world Claude CLI communication with MCP Gemini server"""
|
"""Simulates real-world Claude CLI communication with MCP Gemini server"""
|
||||||
|
|
||||||
def __init__(self, verbose: bool = False, keep_logs: bool = False, selected_tests: list[str] = None):
|
def __init__(
|
||||||
|
self, verbose: bool = False, keep_logs: bool = False, selected_tests: list[str] = None, rebuild: bool = False
|
||||||
|
):
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self.keep_logs = keep_logs
|
self.keep_logs = keep_logs
|
||||||
self.selected_tests = selected_tests or []
|
self.selected_tests = selected_tests or []
|
||||||
|
self.rebuild = rebuild
|
||||||
self.temp_dir = None
|
self.temp_dir = None
|
||||||
self.container_name = "zen-mcp-server"
|
self.container_name = "zen-mcp-server"
|
||||||
self.redis_container = "zen-mcp-redis"
|
self.redis_container = "zen-mcp-redis"
|
||||||
@@ -98,7 +107,7 @@ class CommunicationSimulator:
|
|||||||
return run_test
|
return run_test
|
||||||
|
|
||||||
def setup_test_environment(self) -> bool:
|
def setup_test_environment(self) -> bool:
|
||||||
"""Setup fresh Docker environment"""
|
"""Setup test environment"""
|
||||||
try:
|
try:
|
||||||
self.logger.info("Setting up test environment...")
|
self.logger.info("Setting up test environment...")
|
||||||
|
|
||||||
@@ -106,72 +115,71 @@ class CommunicationSimulator:
|
|||||||
self.temp_dir = tempfile.mkdtemp(prefix="mcp_test_")
|
self.temp_dir = tempfile.mkdtemp(prefix="mcp_test_")
|
||||||
self.logger.debug(f"Created temp directory: {self.temp_dir}")
|
self.logger.debug(f"Created temp directory: {self.temp_dir}")
|
||||||
|
|
||||||
# Setup Docker environment
|
# Only run setup-docker.sh if rebuild is requested
|
||||||
return self._setup_docker()
|
if self.rebuild:
|
||||||
|
if not self._run_setup_docker():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Always verify containers are running (regardless of rebuild)
|
||||||
|
return self._verify_existing_containers()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to setup test environment: {e}")
|
self.logger.error(f"Failed to setup test environment: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _setup_docker(self) -> bool:
|
def _run_setup_docker(self) -> bool:
|
||||||
"""Setup fresh Docker environment"""
|
"""Run the setup-docker.sh script"""
|
||||||
try:
|
try:
|
||||||
self.logger.info("Setting up Docker environment...")
|
self.logger.info("Running setup-docker.sh...")
|
||||||
|
|
||||||
# Stop and remove existing containers
|
# Check if setup-docker.sh exists
|
||||||
self._run_command(["docker", "compose", "down", "--remove-orphans"], check=False, capture_output=True)
|
setup_script = "./setup-docker.sh"
|
||||||
|
if not os.path.exists(setup_script):
|
||||||
|
self.logger.error(f"setup-docker.sh not found at {setup_script}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Clean up any old containers/images
|
# Make sure it's executable
|
||||||
old_containers = [self.container_name, self.redis_container]
|
result = self._run_command(["chmod", "+x", setup_script], capture_output=True)
|
||||||
for container in old_containers:
|
|
||||||
self._run_command(["docker", "stop", container], check=False, capture_output=True)
|
|
||||||
self._run_command(["docker", "rm", container], check=False, capture_output=True)
|
|
||||||
|
|
||||||
# Build and start services
|
|
||||||
self.logger.info("Building Docker images...")
|
|
||||||
result = self._run_command(["docker", "compose", "build", "--no-cache"], capture_output=True)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
self.logger.error(f"Docker build failed: {result.stderr}")
|
self.logger.error(f"Failed to make setup-docker.sh executable: {result.stderr}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.logger.info("Starting Docker services...")
|
# Run the setup script
|
||||||
result = self._run_command(["docker", "compose", "up", "-d"], capture_output=True)
|
result = self._run_command([setup_script], capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
self.logger.error(f"Docker startup failed: {result.stderr}")
|
self.logger.error(f"setup-docker.sh failed: {result.stderr}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Wait for services to be ready
|
self.logger.info("setup-docker.sh completed successfully")
|
||||||
self.logger.info("Waiting for services to be ready...")
|
|
||||||
time.sleep(10) # Give services time to initialize
|
|
||||||
|
|
||||||
# Verify containers are running
|
|
||||||
if not self._verify_containers():
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.logger.info("Docker environment ready")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Docker setup failed: {e}")
|
self.logger.error(f"Failed to run setup-docker.sh: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _verify_containers(self) -> bool:
|
def _verify_existing_containers(self) -> bool:
|
||||||
"""Verify that required containers are running"""
|
"""Verify that required containers are already running (no setup)"""
|
||||||
try:
|
try:
|
||||||
|
self.logger.info("Verifying existing Docker containers...")
|
||||||
|
|
||||||
result = self._run_command(["docker", "ps", "--format", "{{.Names}}"], capture_output=True)
|
result = self._run_command(["docker", "ps", "--format", "{{.Names}}"], capture_output=True)
|
||||||
running_containers = result.stdout.decode().strip().split("\\n")
|
running_containers = result.stdout.decode().strip().split("\n")
|
||||||
|
|
||||||
required = [self.container_name, self.redis_container]
|
required = [self.container_name, self.redis_container]
|
||||||
for container in required:
|
for container in required:
|
||||||
if container not in running_containers:
|
if container not in running_containers:
|
||||||
self.logger.error(f"Container not running: {container}")
|
self.logger.error(f"Required container not running: {container}")
|
||||||
|
self.logger.error(
|
||||||
|
"Please start Docker containers first, or use --rebuild to set them up automatically"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.logger.debug(f"Verified containers running: {required}")
|
self.logger.info(f"All required containers are running: {required}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Container verification failed: {e}")
|
self.logger.error(f"Container verification failed: {e}")
|
||||||
|
self.logger.error("Please ensure Docker is running and containers are available, or use --rebuild")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def simulate_claude_cli_session(self) -> bool:
|
def simulate_claude_cli_session(self) -> bool:
|
||||||
@@ -236,8 +244,8 @@ class CommunicationSimulator:
|
|||||||
self.logger.error(f"Test {test_name} failed with exception: {e}")
|
self.logger.error(f"Test {test_name} failed with exception: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run_individual_test(self, test_name: str, skip_docker_setup: bool = False) -> bool:
|
def run_individual_test(self, test_name: str) -> bool:
|
||||||
"""Run a single test individually with optional Docker setup skip"""
|
"""Run a single test individually"""
|
||||||
try:
|
try:
|
||||||
if test_name not in self.available_tests:
|
if test_name not in self.available_tests:
|
||||||
self.logger.error(f"Unknown test: {test_name}")
|
self.logger.error(f"Unknown test: {test_name}")
|
||||||
@@ -246,11 +254,10 @@ class CommunicationSimulator:
|
|||||||
|
|
||||||
self.logger.info(f"Running individual test: {test_name}")
|
self.logger.info(f"Running individual test: {test_name}")
|
||||||
|
|
||||||
# Setup environment unless skipped
|
# Setup environment
|
||||||
if not skip_docker_setup:
|
if not self.setup_test_environment():
|
||||||
if not self.setup_test_environment():
|
self.logger.error("Environment setup failed")
|
||||||
self.logger.error("Environment setup failed")
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
# Run the single test
|
# Run the single test
|
||||||
test_function = self.available_tests[test_name]
|
test_function = self.available_tests[test_name]
|
||||||
@@ -267,7 +274,7 @@ class CommunicationSimulator:
|
|||||||
self.logger.error(f"Individual test {test_name} failed with exception: {e}")
|
self.logger.error(f"Individual test {test_name} failed with exception: {e}")
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
if not skip_docker_setup and not self.keep_logs:
|
if not self.keep_logs:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
def get_available_tests(self) -> dict[str, str]:
|
def get_available_tests(self) -> dict[str, str]:
|
||||||
@@ -281,9 +288,9 @@ class CommunicationSimulator:
|
|||||||
|
|
||||||
def print_test_summary(self):
|
def print_test_summary(self):
|
||||||
"""Print comprehensive test results summary"""
|
"""Print comprehensive test results summary"""
|
||||||
print("\\n" + "=" * 70)
|
self.logger.info("\n" + "=" * 70)
|
||||||
print("ZEN MCP COMMUNICATION SIMULATOR - TEST RESULTS SUMMARY")
|
self.logger.info("ZEN MCP COMMUNICATION SIMULATOR - TEST RESULTS SUMMARY")
|
||||||
print("=" * 70)
|
self.logger.info("=" * 70)
|
||||||
|
|
||||||
passed_count = sum(1 for result in self.test_results.values() if result)
|
passed_count = sum(1 for result in self.test_results.values() if result)
|
||||||
total_count = len(self.test_results)
|
total_count = len(self.test_results)
|
||||||
@@ -293,25 +300,28 @@ class CommunicationSimulator:
|
|||||||
# Get test description
|
# Get test description
|
||||||
temp_instance = self.test_registry[test_name](verbose=False)
|
temp_instance = self.test_registry[test_name](verbose=False)
|
||||||
description = temp_instance.test_description
|
description = temp_instance.test_description
|
||||||
print(f"{description}: {status}")
|
if result:
|
||||||
|
self.logger.info(f"{description}: {status}")
|
||||||
|
else:
|
||||||
|
self.logger.error(f"{description}: {status}")
|
||||||
|
|
||||||
print(f"\\nOVERALL RESULT: {'SUCCESS' if passed_count == total_count else 'FAILURE'}")
|
if passed_count == total_count:
|
||||||
print(f"{passed_count}/{total_count} tests passed")
|
self.logger.info("\nOVERALL RESULT: SUCCESS")
|
||||||
print("=" * 70)
|
else:
|
||||||
|
self.logger.error("\nOVERALL RESULT: FAILURE")
|
||||||
|
self.logger.info(f"{passed_count}/{total_count} tests passed")
|
||||||
|
self.logger.info("=" * 70)
|
||||||
return passed_count == total_count
|
return passed_count == total_count
|
||||||
|
|
||||||
def run_full_test_suite(self, skip_docker_setup: bool = False) -> bool:
|
def run_full_test_suite(self) -> bool:
|
||||||
"""Run the complete test suite"""
|
"""Run the complete test suite"""
|
||||||
try:
|
try:
|
||||||
self.logger.info("Starting Zen MCP Communication Simulator Test Suite")
|
self.logger.info("Starting Zen MCP Communication Simulator Test Suite")
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
if not skip_docker_setup:
|
if not self.setup_test_environment():
|
||||||
if not self.setup_test_environment():
|
self.logger.error("Environment setup failed")
|
||||||
self.logger.error("Environment setup failed")
|
return False
|
||||||
return False
|
|
||||||
else:
|
|
||||||
self.logger.info("Skipping Docker setup (containers assumed running)")
|
|
||||||
|
|
||||||
# Main simulation
|
# Main simulation
|
||||||
if not self.simulate_claude_cli_session():
|
if not self.simulate_claude_cli_session():
|
||||||
@@ -327,7 +337,7 @@ class CommunicationSimulator:
|
|||||||
self.logger.error(f"Test suite failed: {e}")
|
self.logger.error(f"Test suite failed: {e}")
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
if not self.keep_logs and not skip_docker_setup:
|
if not self.keep_logs:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
@@ -335,11 +345,11 @@ class CommunicationSimulator:
|
|||||||
try:
|
try:
|
||||||
self.logger.info("Cleaning up test environment...")
|
self.logger.info("Cleaning up test environment...")
|
||||||
|
|
||||||
|
# Note: We don't stop Docker services ourselves - let setup-docker.sh handle Docker lifecycle
|
||||||
if not self.keep_logs:
|
if not self.keep_logs:
|
||||||
# Stop Docker services
|
self.logger.info("Test completed. Docker containers left running (use setup-docker.sh to manage)")
|
||||||
self._run_command(["docker", "compose", "down", "--remove-orphans"], check=False, capture_output=True)
|
|
||||||
else:
|
else:
|
||||||
self.logger.info("Keeping Docker services running for log inspection")
|
self.logger.info("Keeping logs and Docker services running for inspection")
|
||||||
|
|
||||||
# Remove temp directory
|
# Remove temp directory
|
||||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||||
@@ -365,15 +375,7 @@ def parse_arguments():
|
|||||||
parser.add_argument("--tests", "-t", nargs="+", help="Specific tests to run (space-separated)")
|
parser.add_argument("--tests", "-t", nargs="+", help="Specific tests to run (space-separated)")
|
||||||
parser.add_argument("--list-tests", action="store_true", help="List available tests and exit")
|
parser.add_argument("--list-tests", action="store_true", help="List available tests and exit")
|
||||||
parser.add_argument("--individual", "-i", help="Run a single test individually")
|
parser.add_argument("--individual", "-i", help="Run a single test individually")
|
||||||
parser.add_argument(
|
parser.add_argument("--rebuild", action="store_true", help="Force rebuild Docker environment using setup-docker.sh")
|
||||||
"--skip-docker",
|
|
||||||
action="store_true",
|
|
||||||
default=True,
|
|
||||||
help="Skip Docker setup (assumes containers are already running) - DEFAULT",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--rebuild-docker", action="store_true", help="Force rebuild Docker environment (overrides --skip-docker)"
|
|
||||||
)
|
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
@@ -381,57 +383,59 @@ def parse_arguments():
|
|||||||
def list_available_tests():
|
def list_available_tests():
|
||||||
"""List all available tests and exit"""
|
"""List all available tests and exit"""
|
||||||
simulator = CommunicationSimulator()
|
simulator = CommunicationSimulator()
|
||||||
print("Available tests:")
|
# Create a simple logger for this function
|
||||||
|
logger = logging.getLogger("list_tests")
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
|
||||||
|
logger.info("Available tests:")
|
||||||
for test_name, description in simulator.get_available_tests().items():
|
for test_name, description in simulator.get_available_tests().items():
|
||||||
print(f" {test_name:<25} - {description}")
|
logger.info(f" {test_name:<25} - {description}")
|
||||||
|
|
||||||
|
|
||||||
def run_individual_test(simulator, test_name, skip_docker):
|
def run_individual_test(simulator, test_name):
|
||||||
"""Run a single test individually"""
|
"""Run a single test individually"""
|
||||||
|
logger = simulator.logger
|
||||||
try:
|
try:
|
||||||
success = simulator.run_individual_test(test_name, skip_docker_setup=skip_docker)
|
success = simulator.run_individual_test(test_name)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
print(f"\\nINDIVIDUAL TEST {test_name.upper()}: PASSED")
|
logger.info(f"\nINDIVIDUAL TEST {test_name.upper()}: PASSED")
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
print(f"\\nINDIVIDUAL TEST {test_name.upper()}: FAILED")
|
logger.error(f"\nINDIVIDUAL TEST {test_name.upper()}: FAILED")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print(f"\\nIndividual test {test_name} interrupted by user")
|
logger.warning(f"\nIndividual test {test_name} interrupted by user")
|
||||||
if not skip_docker:
|
simulator.cleanup()
|
||||||
simulator.cleanup()
|
|
||||||
return 130
|
return 130
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\\nIndividual test {test_name} failed with error: {e}")
|
logger.error(f"\nIndividual test {test_name} failed with error: {e}")
|
||||||
if not skip_docker:
|
simulator.cleanup()
|
||||||
simulator.cleanup()
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def run_test_suite(simulator, skip_docker=False):
|
def run_test_suite(simulator):
|
||||||
"""Run the full test suite or selected tests"""
|
"""Run the full test suite or selected tests"""
|
||||||
|
logger = simulator.logger
|
||||||
try:
|
try:
|
||||||
success = simulator.run_full_test_suite(skip_docker_setup=skip_docker)
|
success = simulator.run_full_test_suite()
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
print("\\nCOMPREHENSIVE MCP COMMUNICATION TEST: PASSED")
|
logger.info("\nCOMPREHENSIVE MCP COMMUNICATION TEST: PASSED")
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
print("\\nCOMPREHENSIVE MCP COMMUNICATION TEST: FAILED")
|
logger.error("\nCOMPREHENSIVE MCP COMMUNICATION TEST: FAILED")
|
||||||
print("Check detailed results above")
|
logger.error("Check detailed results above")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\\nTest interrupted by user")
|
logger.warning("\nTest interrupted by user")
|
||||||
if not skip_docker:
|
simulator.cleanup()
|
||||||
simulator.cleanup()
|
|
||||||
return 130
|
return 130
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\\nUnexpected error: {e}")
|
logger.error(f"\nUnexpected error: {e}")
|
||||||
if not skip_docker:
|
simulator.cleanup()
|
||||||
simulator.cleanup()
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
@@ -445,16 +449,15 @@ def main():
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Initialize simulator consistently for all use cases
|
# Initialize simulator consistently for all use cases
|
||||||
simulator = CommunicationSimulator(verbose=args.verbose, keep_logs=args.keep_logs, selected_tests=args.tests)
|
simulator = CommunicationSimulator(
|
||||||
|
verbose=args.verbose, keep_logs=args.keep_logs, selected_tests=args.tests, rebuild=args.rebuild
|
||||||
|
)
|
||||||
|
|
||||||
# Determine execution mode and run
|
# Determine execution mode and run
|
||||||
# Override skip_docker if rebuild_docker is specified
|
|
||||||
skip_docker = args.skip_docker and not args.rebuild_docker
|
|
||||||
|
|
||||||
if args.individual:
|
if args.individual:
|
||||||
exit_code = run_individual_test(simulator, args.individual, skip_docker)
|
exit_code = run_individual_test(simulator, args.individual)
|
||||||
else:
|
else:
|
||||||
exit_code = run_test_suite(simulator, skip_docker)
|
exit_code = run_test_suite(simulator)
|
||||||
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ import config # noqa: E402
|
|||||||
|
|
||||||
importlib.reload(config)
|
importlib.reload(config)
|
||||||
|
|
||||||
# Set MCP_PROJECT_ROOT to a temporary directory for tests
|
# Set WORKSPACE_ROOT to a temporary directory for tests
|
||||||
# This provides a safe sandbox for file operations during testing
|
# This provides a safe sandbox for file operations during testing
|
||||||
# Create a temporary directory that will be used as the project root for all tests
|
# Create a temporary directory that will be used as the workspace for all tests
|
||||||
test_root = tempfile.mkdtemp(prefix="zen_mcp_test_")
|
test_root = tempfile.mkdtemp(prefix="zen_mcp_test_")
|
||||||
os.environ["MCP_PROJECT_ROOT"] = test_root
|
os.environ["WORKSPACE_ROOT"] = test_root
|
||||||
|
|
||||||
# Configure asyncio for Windows compatibility
|
# Configure asyncio for Windows compatibility
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@@ -55,11 +55,11 @@ ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def project_path(tmp_path):
|
def project_path(tmp_path):
|
||||||
"""
|
"""
|
||||||
Provides a temporary directory within the PROJECT_ROOT sandbox for tests.
|
Provides a temporary directory within the WORKSPACE_ROOT sandbox for tests.
|
||||||
This ensures all file operations during tests are within the allowed directory.
|
This ensures all file operations during tests are within the allowed directory.
|
||||||
"""
|
"""
|
||||||
# Get the test project root
|
# Get the test workspace root
|
||||||
test_root = Path(os.environ.get("MCP_PROJECT_ROOT", "/tmp"))
|
test_root = Path(os.environ.get("WORKSPACE_ROOT", "/tmp"))
|
||||||
|
|
||||||
# Create a subdirectory for this specific test
|
# Create a subdirectory for this specific test
|
||||||
test_dir = test_root / f"test_{tmp_path.name}"
|
test_dir = test_root / f"test_{tmp_path.name}"
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ def test_docker_security_validation():
|
|||||||
original_env = os.environ.copy()
|
original_env = os.environ.copy()
|
||||||
try:
|
try:
|
||||||
os.environ["WORKSPACE_ROOT"] = str(host_workspace)
|
os.environ["WORKSPACE_ROOT"] = str(host_workspace)
|
||||||
os.environ["MCP_PROJECT_ROOT"] = str(host_workspace)
|
|
||||||
|
|
||||||
# Reload the module
|
# Reload the module
|
||||||
importlib.reload(utils.file_utils)
|
importlib.reload(utils.file_utils)
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ class TestPrecommitToolWithMockStore:
|
|||||||
temp_dir, _ = temp_repo
|
temp_dir, _ = temp_repo
|
||||||
tool = Precommit()
|
tool = Precommit()
|
||||||
|
|
||||||
# Mock the Redis client getter and PROJECT_ROOT to allow access to temp files
|
# Mock the Redis client getter and SECURITY_ROOT to allow access to temp files
|
||||||
with (
|
with (
|
||||||
patch("utils.conversation_memory.get_redis_client", return_value=mock_redis),
|
patch("utils.conversation_memory.get_redis_client", return_value=mock_redis),
|
||||||
patch("utils.file_utils.PROJECT_ROOT", Path(temp_dir).resolve()),
|
patch("utils.file_utils.SECURITY_ROOT", Path(temp_dir).resolve()),
|
||||||
):
|
):
|
||||||
yield tool
|
yield tool
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TestFileUtils:
|
|||||||
# Try to read a file outside the project root
|
# Try to read a file outside the project root
|
||||||
content, tokens = read_file_content("/etc/passwd")
|
content, tokens = read_file_content("/etc/passwd")
|
||||||
assert "--- ERROR ACCESSING FILE:" in content
|
assert "--- ERROR ACCESSING FILE:" in content
|
||||||
assert "Path outside project root" in content
|
assert "Path outside workspace" in content
|
||||||
assert tokens > 0
|
assert tokens > 0
|
||||||
|
|
||||||
def test_read_file_content_relative_path_rejected(self):
|
def test_read_file_content_relative_path_rejected(self):
|
||||||
|
|||||||
@@ -82,32 +82,18 @@ if WORKSPACE_ROOT:
|
|||||||
f"Please set WORKSPACE_ROOT to a specific project directory."
|
f"Please set WORKSPACE_ROOT to a specific project directory."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get project root from environment or use current directory
|
# Security boundary
|
||||||
# This defines the sandbox directory where file access is allowed
|
# In Docker: use /workspace (container directory)
|
||||||
#
|
# In tests/direct mode: use WORKSPACE_ROOT (host directory)
|
||||||
# Simplified Security model:
|
if CONTAINER_WORKSPACE.exists():
|
||||||
# 1. If MCP_PROJECT_ROOT is explicitly set, use it as sandbox (override)
|
# Running in Docker container
|
||||||
# 2. If WORKSPACE_ROOT is set (Docker mode), auto-use /workspace as sandbox
|
SECURITY_ROOT = CONTAINER_WORKSPACE
|
||||||
# 3. Otherwise, use home directory (direct usage)
|
elif WORKSPACE_ROOT:
|
||||||
env_root = os.environ.get("MCP_PROJECT_ROOT")
|
# Running in tests or direct mode with WORKSPACE_ROOT set
|
||||||
if env_root:
|
SECURITY_ROOT = Path(WORKSPACE_ROOT).resolve()
|
||||||
# If explicitly set, use it as sandbox (allows custom override)
|
|
||||||
PROJECT_ROOT = Path(env_root).resolve()
|
|
||||||
elif WORKSPACE_ROOT and CONTAINER_WORKSPACE.exists():
|
|
||||||
# Running in Docker with workspace mounted - auto-use /workspace
|
|
||||||
PROJECT_ROOT = CONTAINER_WORKSPACE
|
|
||||||
else:
|
else:
|
||||||
# Running directly on host - default to home directory for normal usage
|
# Fallback for backward compatibility (should not happen in normal usage)
|
||||||
# This allows access to any file under the user's home directory
|
SECURITY_ROOT = Path.home()
|
||||||
PROJECT_ROOT = Path.home()
|
|
||||||
|
|
||||||
# Additional security check for explicit PROJECT_ROOT
|
|
||||||
if env_root and PROJECT_ROOT.parent == PROJECT_ROOT:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Security Error: MCP_PROJECT_ROOT cannot be the filesystem root. "
|
|
||||||
"This would give access to the entire filesystem. "
|
|
||||||
"Please set MCP_PROJECT_ROOT to a specific directory."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Directories to exclude from recursive file search
|
# Directories to exclude from recursive file search
|
||||||
@@ -293,15 +279,15 @@ def resolve_and_validate_path(path_str: str) -> Path:
|
|||||||
# Step 5: Security Policy - Ensure the resolved path is within PROJECT_ROOT
|
# Step 5: Security Policy - Ensure the resolved path is within PROJECT_ROOT
|
||||||
# This prevents directory traversal attacks (e.g., /project/../../../etc/passwd)
|
# This prevents directory traversal attacks (e.g., /project/../../../etc/passwd)
|
||||||
try:
|
try:
|
||||||
resolved_path.relative_to(PROJECT_ROOT)
|
resolved_path.relative_to(SECURITY_ROOT)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Provide detailed error for debugging while avoiding information disclosure
|
# Provide detailed error for debugging while avoiding information disclosure
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Access denied - path outside project root. "
|
f"Access denied - path outside workspace. "
|
||||||
f"Requested: {path_str}, Resolved: {resolved_path}, Root: {PROJECT_ROOT}"
|
f"Requested: {path_str}, Resolved: {resolved_path}, Workspace: {SECURITY_ROOT}"
|
||||||
)
|
)
|
||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
f"Path outside project root: {path_str}\nProject root: {PROJECT_ROOT}\nResolved path: {resolved_path}"
|
f"Path outside workspace: {path_str}\nWorkspace: {SECURITY_ROOT}\nResolved path: {resolved_path}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return resolved_path
|
return resolved_path
|
||||||
@@ -358,16 +344,16 @@ def expand_paths(paths: list[str], extensions: Optional[set[str]] = None) -> lis
|
|||||||
if not path_obj.exists():
|
if not path_obj.exists():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Safety check: Prevent reading entire home directory or workspace root
|
# Safety check: Prevent reading entire workspace root
|
||||||
# This could expose too many files and cause performance issues
|
# This could expose too many files and cause performance issues
|
||||||
if path_obj.is_dir():
|
if path_obj.is_dir():
|
||||||
resolved_project_root = PROJECT_ROOT.resolve()
|
resolved_workspace = SECURITY_ROOT.resolve()
|
||||||
resolved_path = path_obj.resolve()
|
resolved_path = path_obj.resolve()
|
||||||
|
|
||||||
# Check if this is the entire project root/home directory
|
# Check if this is the entire workspace root directory
|
||||||
if resolved_path == resolved_project_root:
|
if resolved_path == resolved_workspace:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Ignoring request to read entire project root directory: {path}. "
|
f"Ignoring request to read entire workspace directory: {path}. "
|
||||||
f"Please specify individual files or subdirectories instead."
|
f"Please specify individual files or subdirectories instead."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user