Spaces:
Paused
Paused
| """Tests for config.yaml structure validation (validate_config_structure).""" | |
| import pytest | |
| from hermes_cli.config import validate_config_structure, ConfigIssue | |
| class TestCustomProvidersValidation: | |
| """custom_providers must be a YAML list, not a dict.""" | |
| def test_dict_instead_of_list(self): | |
| """The exact Discord user scenario — custom_providers as flat dict.""" | |
| issues = validate_config_structure({ | |
| "custom_providers": { | |
| "name": "Generativelanguage.googleapis.com", | |
| "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", | |
| "api_key": "xxx", | |
| "model": "models/gemini-2.5-flash", | |
| "rate_limit_delay": 2.0, | |
| "fallback_model": { | |
| "provider": "openrouter", | |
| "model": "qwen/qwen3.6-plus:free", | |
| }, | |
| }, | |
| "fallback_providers": [], | |
| }) | |
| errors = [i for i in issues if i.severity == "error"] | |
| assert any("dict" in i.message and "list" in i.message for i in errors), ( | |
| "Should detect custom_providers as dict instead of list" | |
| ) | |
| def test_dict_detects_misplaced_fields(self): | |
| """When custom_providers is a dict, detect fields that look misplaced.""" | |
| issues = validate_config_structure({ | |
| "custom_providers": { | |
| "name": "test", | |
| "base_url": "https://example.com", | |
| "api_key": "xxx", | |
| }, | |
| }) | |
| warnings = [i for i in issues if i.severity == "warning"] | |
| # Should flag base_url, api_key as looking like custom_providers entry fields | |
| misplaced = [i for i in warnings if "custom_providers entry fields" in i.message] | |
| assert len(misplaced) == 1 | |
| def test_dict_detects_nested_fallback(self): | |
| """When fallback_model gets swallowed into custom_providers dict.""" | |
| issues = validate_config_structure({ | |
| "custom_providers": { | |
| "name": "test", | |
| "fallback_model": {"provider": "openrouter", "model": "test"}, | |
| }, | |
| }) | |
| errors = [i for i in issues if i.severity == "error"] | |
| assert any("fallback_model" in i.message and "inside" in i.message for i in errors) | |
| def test_valid_list_no_issues(self): | |
| """Properly formatted custom_providers should produce no issues.""" | |
| issues = validate_config_structure({ | |
| "custom_providers": [ | |
| {"name": "gemini", "base_url": "https://example.com/v1"}, | |
| ], | |
| "model": {"provider": "custom", "default": "test"}, | |
| }) | |
| assert len(issues) == 0 | |
| def test_list_entry_missing_name(self): | |
| """List entry without name should warn.""" | |
| issues = validate_config_structure({ | |
| "custom_providers": [{"base_url": "https://example.com/v1"}], | |
| "model": {"provider": "custom"}, | |
| }) | |
| assert any("missing 'name'" in i.message for i in issues) | |
| def test_list_entry_missing_base_url(self): | |
| """List entry without base_url should warn.""" | |
| issues = validate_config_structure({ | |
| "custom_providers": [{"name": "test"}], | |
| "model": {"provider": "custom"}, | |
| }) | |
| assert any("missing 'base_url'" in i.message for i in issues) | |
| def test_list_entry_not_dict(self): | |
| """Non-dict list entries should warn.""" | |
| issues = validate_config_structure({ | |
| "custom_providers": ["not-a-dict"], | |
| "model": {"provider": "custom"}, | |
| }) | |
| assert any("not a dict" in i.message for i in issues) | |
| def test_none_custom_providers_no_issues(self): | |
| """No custom_providers at all should be fine.""" | |
| issues = validate_config_structure({ | |
| "model": {"provider": "openrouter"}, | |
| }) | |
| assert len(issues) == 0 | |
| class TestFallbackModelValidation: | |
| """fallback_model should be a top-level dict with provider + model.""" | |
| def test_missing_provider(self): | |
| issues = validate_config_structure({ | |
| "fallback_model": {"model": "anthropic/claude-sonnet-4"}, | |
| }) | |
| assert any("missing 'provider'" in i.message for i in issues) | |
| def test_missing_model(self): | |
| issues = validate_config_structure({ | |
| "fallback_model": {"provider": "openrouter"}, | |
| }) | |
| assert any("missing 'model'" in i.message for i in issues) | |
| def test_valid_fallback(self): | |
| issues = validate_config_structure({ | |
| "fallback_model": { | |
| "provider": "openrouter", | |
| "model": "anthropic/claude-sonnet-4", | |
| }, | |
| }) | |
| # Only fallback-related issues should be absent | |
| fb_issues = [i for i in issues if "fallback" in i.message.lower()] | |
| assert len(fb_issues) == 0 | |
| def test_non_dict_fallback(self): | |
| issues = validate_config_structure({ | |
| "fallback_model": "openrouter:anthropic/claude-sonnet-4", | |
| }) | |
| assert any("should be a dict" in i.message for i in issues) | |
| def test_empty_fallback_dict_no_issues(self): | |
| """Empty fallback_model dict means disabled — no warnings needed.""" | |
| issues = validate_config_structure({ | |
| "fallback_model": {}, | |
| }) | |
| fb_issues = [i for i in issues if "fallback" in i.message.lower()] | |
| assert len(fb_issues) == 0 | |
| class TestMissingModelSection: | |
| """Warn when custom_providers exists but model section is missing.""" | |
| def test_custom_providers_without_model(self): | |
| issues = validate_config_structure({ | |
| "custom_providers": [ | |
| {"name": "test", "base_url": "https://example.com/v1"}, | |
| ], | |
| }) | |
| assert any("no 'model' section" in i.message for i in issues) | |
| def test_custom_providers_with_model(self): | |
| issues = validate_config_structure({ | |
| "custom_providers": [ | |
| {"name": "test", "base_url": "https://example.com/v1"}, | |
| ], | |
| "model": {"provider": "custom", "default": "test-model"}, | |
| }) | |
| # Should not warn about missing model section | |
| assert not any("no 'model' section" in i.message for i in issues) | |
| class TestConfigIssueDataclass: | |
| """ConfigIssue should be a proper dataclass.""" | |
| def test_fields(self): | |
| issue = ConfigIssue(severity="error", message="test msg", hint="test hint") | |
| assert issue.severity == "error" | |
| assert issue.message == "test msg" | |
| assert issue.hint == "test hint" | |
| def test_equality(self): | |
| a = ConfigIssue("error", "msg", "hint") | |
| b = ConfigIssue("error", "msg", "hint") | |
| assert a == b | |