Skip to main content
Glama
test_config.py23.9 kB
import tempfile from pathlib import Path import pytest from hooks_mcp.config import ConfigError, HooksMCPConfig class TestConfig: """Test configuration parsing functionality.""" def test_valid_config_minimal(self): """Test parsing a minimal valid configuration.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert config.server_name == "HooksMCP" assert ( config.server_description == "Project-specific development tools and prompts exposed via MCP" ) assert len(config.actions) == 1 action = config.actions[0] assert action.name == "test" assert action.description == "Run tests" assert action.command == "python -m pytest" assert action.run_path is None assert len(action.parameters) == 0 def test_valid_config_full(self): """Test parsing a full valid configuration.""" yaml_content = """ server_name: "MyProjectTools" server_description: "Development tools for MyProject" actions: - name: "all_tests" description: "Run all tests" command: "python -m pytest" run_path: "tests" - name: "test_file" description: "Run tests in a specific file" command: "python -m pytest $TEST_FILE" parameters: - name: "TEST_FILE" type: "project_file_path" description: "Path to test file" default: "./tests" - name: "lint" description: "Lint the code" command: "flake8 src" parameters: - name: "SKIP_CHECKS" type: "insecure_string" description: "Checks to skip" - name: "typecheck" description: "Typecheck the code" command: "mypy src" parameters: - name: "MY_API_KEY" type: "required_env_var" description: "API key for my service" - name: "VERBOSE" type: "optional_env_var" description: "Enable verbose output" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert config.server_name == "MyProjectTools" assert config.server_description == "Development tools for MyProject" assert len(config.actions) == 4 # Check first action action1 = config.actions[0] assert action1.name == "all_tests" assert action1.description == "Run all tests" assert action1.command == "python -m pytest" assert action1.run_path == "tests" assert len(action1.parameters) == 0 # Check second action action2 = config.actions[1] assert action2.name == "test_file" assert action2.description == "Run tests in a specific file" assert action2.command == "python -m pytest $TEST_FILE" assert len(action2.parameters) == 1 param1 = action2.parameters[0] assert param1.name == "TEST_FILE" assert param1.type == "project_file_path" assert param1.description == "Path to test file" assert param1.default == "./tests" # Check third action action3 = config.actions[2] assert action3.name == "lint" assert action3.description == "Lint the code" assert action3.command == "flake8 src" assert len(action3.parameters) == 1 param2 = action3.parameters[0] assert param2.name == "SKIP_CHECKS" assert param2.type == "insecure_string" assert param2.description == "Checks to skip" assert param2.default is None # Check fourth action action4 = config.actions[3] assert action4.name == "typecheck" assert action4.description == "Typecheck the code" assert action4.command == "mypy src" assert len(action4.parameters) == 2 param3 = action4.parameters[0] assert param3.name == "MY_API_KEY" assert param3.type == "required_env_var" assert param3.description == "API key for my service" assert param3.default is None param4 = action4.parameters[1] assert param4.name == "VERBOSE" assert param4.type == "optional_env_var" assert param4.description == "Enable verbose output" assert param4.default is None def test_valid_config_missing_actions(self): """Test that config without actions is valid.""" yaml_content = """ server_name: "MyProjectTools" server_description: "Development tools for MyProject" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert config.server_name == "MyProjectTools" assert config.server_description == "Development tools for MyProject" assert len(config.actions) == 0 def test_invalid_config_invalid_parameter_type(self): """Test that config with invalid parameter type raises an error.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" parameters: - name: "TEST_FILE" type: "invalid_type" description: "Path to test file" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "Invalid parameter type" in str(context.value) assert "invalid_type" in str(context.value) def test_invalid_config_missing_required_fields(self): """Test that config with missing required fields raises an error.""" yaml_content = """ actions: - description: "Run tests" command: "python -m pytest" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "'name' is required" in str(context.value) def test_config_validate_required_env_vars(self): """Test validation of required environment variables.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" parameters: - name: "API_KEY" type: "required_env_var" description: "API key" - name: "OPTIONAL_VAR" type: "optional_env_var" description: "Optional variable" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() # Initially, no env vars are set, so API_KEY should be missing missing_vars = config.validate_required_env_vars() assert "API_KEY" in missing_vars assert "OPTIONAL_VAR" not in missing_vars # Set the required env var and check again import os os.environ["API_KEY"] = "test_key" missing_vars = config.validate_required_env_vars() assert "API_KEY" not in missing_vars # Clean up del os.environ["API_KEY"] def test_config_with_timeout(self): """Test parsing a configuration with timeout parameter.""" yaml_content = """ actions: - name: "test_with_timeout" description: "Test action with custom timeout" command: "sleep 10" timeout: 30 - name: "test_without_timeout" description: "Test action without timeout" command: "echo Hello" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert len(config.actions) == 2 # Check action with timeout action1 = config.actions[0] assert action1.name == "test_with_timeout" assert action1.description == "Test action with custom timeout" assert action1.command == "sleep 10" assert action1.timeout == 30 # Check action without timeout (should use default) action2 = config.actions[1] assert action2.name == "test_without_timeout" assert action2.description == "Test action without timeout" assert action2.command == "echo Hello" assert action2.timeout == 60 # Default timeout def test_valid_config_with_prompts_inline(self): """Test parsing a configuration with inline prompts.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "code_review" description: "Review code for best practices" prompt: "Please review this code for best practices and potential bugs." - name: "test_generation" description: "Generate unit tests for code" prompt: "Generate unit tests for the following code:\n$CODE_SNIPPET" arguments: - name: "CODE_SNIPPET" description: "The code to generate tests for" required: true """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() temp_path = Path(f.name) try: config = HooksMCPConfig.from_yaml(str(temp_path)) finally: # Clean up temp_path.unlink() assert len(config.prompts) == 2 # Check first prompt prompt1 = config.prompts[0] assert prompt1.name == "code_review" assert prompt1.description == "Review code for best practices" assert ( prompt1.prompt_text == "Please review this code for best practices and potential bugs." ) assert prompt1.prompt_file is None assert len(prompt1.arguments) == 0 # Check second prompt prompt2 = config.prompts[1] assert prompt2.name == "test_generation" assert prompt2.description == "Generate unit tests for code" # Check that the prompt text contains the expected content (exact formatting may vary) assert prompt2.prompt_text is not None assert "Generate unit tests for the following code:" in prompt2.prompt_text assert "$CODE_SNIPPET" in prompt2.prompt_text assert prompt2.prompt_file is None assert len(prompt2.arguments) == 1 arg1 = prompt2.arguments[0] assert arg1.name == "CODE_SNIPPET" assert arg1.description == "The code to generate tests for" assert arg1.required def test_valid_config_with_prompts_file(self): """Test parsing a configuration with file-based prompts.""" # Create a temporary directory for both files with tempfile.TemporaryDirectory() as temp_dir: temp_dir_path = Path(temp_dir) # Create the prompt file prompt_file_path = temp_dir_path / "test_prompt.md" with open(prompt_file_path, "w") as prompt_file: prompt_file.write( "# Code Analysis Prompt\n\nAnalyze the following code snippet." ) # Create the config file yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "code_analysis" description: "Analyze code for quality and security" prompt-file: "test_prompt.md" - name: "architecture_review" description: "Review system architecture" prompt-file: "test_prompt.md" arguments: - name: "SYSTEM_COMPONENT" description: "The system component to review" required: false """ config_file_path = temp_dir_path / "test_config.yaml" with open(config_file_path, "w") as config_file: config_file.write(yaml_content) try: config = HooksMCPConfig.from_yaml(str(config_file_path)) finally: # Clean up config_file_path.unlink() assert len(config.prompts) == 2 # Check first prompt prompt1 = config.prompts[0] assert prompt1.name == "code_analysis" assert prompt1.description == "Analyze code for quality and security" assert prompt1.prompt_text is None assert prompt1.prompt_file == "test_prompt.md" assert len(prompt1.arguments) == 0 # Check second prompt prompt2 = config.prompts[1] assert prompt2.name == "architecture_review" assert prompt2.description == "Review system architecture" assert prompt2.prompt_text is None assert prompt2.prompt_file == "test_prompt.md" assert len(prompt2.arguments) == 1 arg1 = prompt2.arguments[0] assert arg1.name == "SYSTEM_COMPONENT" assert arg1.description == "The system component to review" assert not arg1.required def test_invalid_config_prompt_missing_required_fields(self): """Test that prompts with missing required fields raise an error.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - description: "A prompt without a name" prompt: "Some prompt content" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "'name' is required for each prompt" in str(context.value) def test_invalid_config_prompt_no_content(self): """Test that prompts without content raise an error.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "invalid_prompt" description: "A prompt with no content" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "must specify either 'prompt' or 'prompt-file'" in str(context.value) def test_invalid_config_prompt_both_content_types(self): """Test that prompts with both content types raise an error.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "invalid_prompt" description: "A prompt with both content types" prompt: "Inline prompt content" prompt-file: "./some_file.md" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "cannot specify both 'prompt' and 'prompt-file'" in str(context.value) def test_invalid_config_prompt_name_too_long(self): """Test that prompts with names exceeding 32 characters raise an error.""" long_name = "a" * 33 # 33 characters, exceeds limit of 32 yaml_content = f""" actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "{long_name}" description: "A prompt with a very long name" prompt: "Some prompt content" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "exceeds 32 character limit" in str(context.value) def test_invalid_config_prompt_description_too_long(self): """Test that prompts with descriptions exceeding 256 characters raise an error.""" long_description = "A" * 257 # 257 characters, exceeds limit of 256 yaml_content = f""" actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "test_prompt" description: "{long_description}" prompt: "Some prompt content" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "exceeds 256 character limit" in str(context.value) def test_invalid_config_prompt_file_not_found(self): """Test that prompts with non-existent files raise an error.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "missing_file_prompt" description: "A prompt referencing a missing file" prompt-file: "./non_existent_file.md" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert ( "Prompt file './non_existent_file.md' for prompt 'missing_file_prompt' not found" in str(context.value) ) def test_invalid_config_prompt_argument_missing_name(self): """Test that prompt arguments without names raise an error.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "test_prompt" description: "A prompt with invalid arguments" prompt: "Some content with $ARG" arguments: - description: "Argument without name" required: true """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert "'name' is required for each prompt argument" in str(context.value) def test_valid_config_get_prompt_tool_filter(self): """Test parsing a configuration with get_prompt_tool_filter.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "prompt1" description: "First prompt" prompt: "Content of first prompt" - name: "prompt2" description: "Second prompt" prompt: "Content of second prompt" get_prompt_tool_filter: - "prompt1" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert len(config.prompts) == 2 assert config.get_prompt_tool_filter == ["prompt1"] def test_invalid_config_get_prompt_tool_filter_invalid_name(self): """Test that get_prompt_tool_filter with invalid prompt names raises an error.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "prompt1" description: "First prompt" prompt: "Content of first prompt" get_prompt_tool_filter: - "nonexistent_prompt" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() with pytest.raises(ConfigError) as context: HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert ( "Prompt 'nonexistent_prompt' in get_prompt_tool_filter not found in prompts list" in str(context.value) ) def test_config_get_prompt_tool_filter_empty_list(self): """Test that empty get_prompt_tool_filter list is handled correctly.""" yaml_content = """ actions: - name: "test" description: "Run tests" command: "python -m pytest" prompts: - name: "prompt1" description: "First prompt" prompt: "Content of first prompt" get_prompt_tool_filter: [] """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert len(config.prompts) == 1 assert config.get_prompt_tool_filter == [] def test_valid_config_prompts_only(self): """Test parsing a configuration with only prompts and no actions.""" yaml_content = """ prompts: - name: "code_review" description: "Review code for best practices" prompt: "Please review this code for best practices and potential bugs." - name: "test_generation" description: "Generate unit tests for code" prompt: "Generate unit tests for the following code:\n$CODE_SNIPPET" arguments: - name: "CODE_SNIPPET" description: "The code to generate tests for" required: true """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = HooksMCPConfig.from_yaml(f.name) # Clean up Path(f.name).unlink() assert config.server_name == "HooksMCP" assert ( config.server_description == "Project-specific development tools and prompts exposed via MCP" ) assert len(config.actions) == 0 assert len(config.prompts) == 2 # Check first prompt prompt1 = config.prompts[0] assert prompt1.name == "code_review" assert prompt1.description == "Review code for best practices" assert ( prompt1.prompt_text == "Please review this code for best practices and potential bugs." ) assert prompt1.prompt_file is None assert len(prompt1.arguments) == 0 # Check second prompt prompt2 = config.prompts[1] assert prompt2.name == "test_generation" assert prompt2.description == "Generate unit tests for code" assert prompt2.prompt_text is not None assert "$CODE_SNIPPET" in prompt2.prompt_text assert prompt2.prompt_file is None assert len(prompt2.arguments) == 1 arg1 = prompt2.arguments[0] assert arg1.name == "CODE_SNIPPET" assert arg1.description == "The code to generate tests for" assert arg1.required

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/scosman/actions_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server