""" HAIM Test Suite — Configuration Tests """ import os import tempfile from pathlib import Path import pytest import yaml from mnemocore.core.config import ( HAIMConfig, load_config, get_config, reset_config, TierConfig, LTPConfig, ) from mnemocore.core.exceptions import ConfigurationError @pytest.fixture(autouse=True) def clean_config(): """Reset global config singleton between tests.""" reset_config() yield reset_config() @pytest.fixture def sample_config_path(tmp_path): """Create a temporary config.yaml.""" config_data = { "haim": { "version": "3.0-test", "dimensionality": 1024, # Small for tests "encoding": {"mode": "binary", "token_method": "bundle"}, "tiers": { "hot": {"max_memories": 100, "ltp_threshold_min": 0.7}, "warm": { "max_memories": 1000, "ltp_threshold_min": 0.3, "consolidation_interval_hours": 1, "storage_backend": "mmap", }, "cold": { "max_memories": 0, "ltp_threshold_min": 0.0, "storage_backend": "filesystem", }, }, "ltp": { "initial_importance": 0.5, "decay_lambda": 0.01, "permanence_threshold": 0.95, "half_life_days": 30.0, }, "hysteresis": {"promote_delta": 0.15, "demote_delta": 0.10}, "redis": {"url": "redis://localhost:6379/0"}, "qdrant": {"url": "http://localhost:6333"}, "gpu": {"enabled": False}, "observability": {"log_level": "DEBUG"}, "paths": {"data_dir": str(tmp_path / "data")}, } } config_path = tmp_path / "config.yaml" with open(config_path, "w") as f: yaml.dump(config_data, f) return config_path class TestLoadConfig: def test_load_from_yaml(self, sample_config_path): config = load_config(sample_config_path) assert config.version == "3.0-test" assert config.dimensionality == 1024 def test_default_values_when_no_file(self, tmp_path): missing_path = tmp_path / "nonexistent.yaml" config = load_config(missing_path) assert config.dimensionality == 16384 assert config.version == "4.5" def test_dimensionality_must_be_multiple_of_64(self, tmp_path): bad_config = {"haim": {"dimensionality": 100}} path = tmp_path / "bad.yaml" with open(path, "w") as f: yaml.dump(bad_config, f) with pytest.raises(ConfigurationError, match="multiple of 64"): load_config(path) def test_encoding_mode(self, sample_config_path): config = load_config(sample_config_path) assert config.encoding.mode == "binary" assert config.encoding.token_method == "bundle" def test_tier_config(self, sample_config_path): config = load_config(sample_config_path) assert config.tiers_hot.max_memories == 100 assert config.tiers_hot.ltp_threshold_min == 0.7 assert config.tiers_warm.storage_backend == "mmap" assert config.tiers_warm.consolidation_interval_hours == 1 def test_ltp_config(self, sample_config_path): config = load_config(sample_config_path) assert config.ltp.decay_lambda == 0.01 assert config.ltp.permanence_threshold == 0.95 def test_hysteresis_config(self, sample_config_path): config = load_config(sample_config_path) assert config.hysteresis.promote_delta == 0.15 assert config.hysteresis.demote_delta == 0.10 class TestEnvironmentOverrides: def test_dimensionality_override(self, sample_config_path): os.environ["HAIM_DIMENSIONALITY"] = "2048" try: config = load_config(sample_config_path) assert config.dimensionality == 2048 finally: del os.environ["HAIM_DIMENSIONALITY"] def test_redis_url_override(self, sample_config_path): os.environ["HAIM_REDIS_URL"] = "redis://custom:6380/1" try: config = load_config(sample_config_path) assert config.redis.url == "redis://custom:6380/1" finally: del os.environ["HAIM_REDIS_URL"] def test_gpu_enabled_override(self, sample_config_path): os.environ["HAIM_GPU_ENABLED"] = "true" try: config = load_config(sample_config_path) assert config.gpu.enabled is True finally: del os.environ["HAIM_GPU_ENABLED"] def test_log_level_override(self, sample_config_path): os.environ["HAIM_LOG_LEVEL"] = "WARNING" try: config = load_config(sample_config_path) assert config.observability.log_level == "WARNING" finally: del os.environ["HAIM_LOG_LEVEL"] def test_mcp_enabled_override(self, sample_config_path): os.environ["HAIM_MCP_ENABLED"] = "true" try: config = load_config(sample_config_path) assert config.mcp.enabled is True finally: del os.environ["HAIM_MCP_ENABLED"] def test_mcp_api_base_url_override(self, sample_config_path): os.environ["HAIM_MCP_API_BASE_URL"] = "http://localhost:8200" try: config = load_config(sample_config_path) assert config.mcp.api_base_url == "http://localhost:8200" finally: del os.environ["HAIM_MCP_API_BASE_URL"] class TestConfigSingleton: def test_get_config_returns_same_instance(self): config_a = get_config() config_b = get_config() assert config_a is config_b def test_reset_clears_singleton(self): config_a = get_config() reset_config() config_b = get_config() # New instance after reset (they're equal but not same object) assert config_a is not config_b def test_config_is_frozen(self): config = get_config() with pytest.raises(AttributeError): config.dimensionality = 9999 class TestConfigValidation: def test_valid_dimensionalities(self, tmp_path): for dim in [64, 128, 1024, 16384]: data = {"haim": {"dimensionality": dim}} path = tmp_path / f"config_{dim}.yaml" with open(path, "w") as f: yaml.dump(data, f) config = load_config(path) assert config.dimensionality == dim def test_invalid_dimensionalities(self, tmp_path): for dim in [100, 1000, 10000, 15000]: data = {"haim": {"dimensionality": dim}} path = tmp_path / f"config_{dim}.yaml" with open(path, "w") as f: yaml.dump(data, f) with pytest.raises(ConfigurationError): load_config(path) class TestSecurityOverrides: def test_redis_password_override(self, sample_config_path): os.environ["HAIM_REDIS_PASSWORD"] = "secret_password" try: config = load_config(sample_config_path) assert config.redis.password == "secret_password" finally: del os.environ["HAIM_REDIS_PASSWORD"] def test_qdrant_api_key_override(self, sample_config_path): os.environ["HAIM_QDRANT_API_KEY"] = "secret_api_key" try: config = load_config(sample_config_path) assert config.qdrant.api_key == "secret_api_key" finally: del os.environ["HAIM_QDRANT_API_KEY"] def test_config_file_values(self, tmp_path): """Test that values can also be loaded from yaml directly.""" config_data = { "haim": { "dimensionality": 1024, "redis": { "url": "redis://localhost:6379/0", "password": "yaml_password" }, "qdrant": { "url": "http://localhost:6333", "api_key": "yaml_api_key" } } } config_path = tmp_path / "config_security.yaml" with open(config_path, "w") as f: yaml.dump(config_data, f) config = load_config(config_path) assert config.redis.password == "yaml_password" assert config.qdrant.api_key == "yaml_api_key" class TestMCPConfig: def test_mcp_defaults(self, sample_config_path): config = load_config(sample_config_path) assert config.mcp.enabled is False assert config.mcp.transport == "stdio" assert "memory_health" in config.mcp.allow_tools def test_mcp_config_file_values(self, tmp_path): config_data = { "haim": { "dimensionality": 1024, "mcp": { "enabled": True, "transport": "sse", "host": "0.0.0.0", "port": 8123, "api_base_url": "http://localhost:8100", "timeout_seconds": 20, "allow_tools": ["memory_health", "memory_stats"], }, } } config_path = tmp_path / "config_mcp.yaml" with open(config_path, "w") as f: yaml.dump(config_data, f) config = load_config(config_path) assert config.mcp.enabled is True assert config.mcp.transport == "sse" assert config.mcp.port == 8123 assert config.mcp.allow_tools == ["memory_health", "memory_stats"]