From 4403f490ba3cf75b9c888a3188a8b568d17cea5e Mon Sep 17 00:00:00 2001 From: kimi Date: Sat, 21 Mar 2026 11:02:56 -0400 Subject: [PATCH] feat: add matrix config loader utility - Add MatrixConfig dataclass with lighting, environment, features, agents sections - Implement load_from_yaml() with sensible defaults for missing keys - Default lighting: warm amber ambient (#FFAA55) + cool blue accents - Default features: all enabled - Add comprehensive unit tests Fixes #680 --- config/matrix.yaml | 17 +- src/infrastructure/matrix_config.py | 266 ++++++++++++++++++++++ tests/unit/test_matrix_config.py | 331 ++++++++++++++++++++++++++++ 3 files changed, 608 insertions(+), 6 deletions(-) create mode 100644 src/infrastructure/matrix_config.py create mode 100644 tests/unit/test_matrix_config.py diff --git a/config/matrix.yaml b/config/matrix.yaml index 684df51b..dd2b27bc 100644 --- a/config/matrix.yaml +++ b/config/matrix.yaml @@ -2,22 +2,22 @@ # Serves lighting, environment, and feature settings to the Matrix frontend. lighting: - ambient_color: "#1a1a2e" - ambient_intensity: 0.4 + ambient_color: "#FFAA55" # Warm amber (Workshop warmth) + ambient_intensity: 0.5 point_lights: - - color: "#FFD700" + - color: "#FFAA55" # Warm amber (Workshop center light) intensity: 1.2 position: { x: 0, y: 5, z: 0 } - - color: "#3B82F6" + - color: "#3B82F6" # Cool blue (Matrix accent) intensity: 0.8 position: { x: -5, y: 3, z: -5 } - - color: "#A855F7" + - color: "#A855F7" # Purple accent intensity: 0.6 position: { x: 5, y: 3, z: 5 } environment: rain_enabled: false - starfield_enabled: true + starfield_enabled: true # Cool blue starfield (Matrix feel) fog_color: "#0f0f23" fog_density: 0.02 @@ -26,3 +26,8 @@ features: visitor_avatars: true pip_familiar: true workshop_portal: true + +agents: + default_count: 5 + max_count: 20 + agents: [] diff --git a/src/infrastructure/matrix_config.py b/src/infrastructure/matrix_config.py new file mode 100644 index 00000000..211ab6d4 --- /dev/null +++ b/src/infrastructure/matrix_config.py @@ -0,0 +1,266 @@ +"""Matrix configuration loader utility. + +Provides a typed dataclass for Matrix world configuration and a loader +that fetches settings from YAML with sensible defaults. +""" + +import logging +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import yaml + +logger = logging.getLogger(__name__) + + +@dataclass +class PointLight: + """A single point light in the Matrix world.""" + + color: str = "#FFFFFF" + intensity: float = 1.0 + position: dict[str, float] = field(default_factory=lambda: {"x": 0, "y": 0, "z": 0}) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "PointLight": + """Create a PointLight from a dictionary with defaults.""" + return cls( + color=data.get("color", "#FFFFFF"), + intensity=data.get("intensity", 1.0), + position=data.get("position", {"x": 0, "y": 0, "z": 0}), + ) + + +def _default_point_lights_factory() -> list[PointLight]: + """Factory function for default point lights.""" + return [ + PointLight( + color="#FFAA55", # Warm amber (Workshop) + intensity=1.2, + position={"x": 0, "y": 5, "z": 0}, + ), + PointLight( + color="#3B82F6", # Cool blue (Matrix) + intensity=0.8, + position={"x": -5, "y": 3, "z": -5}, + ), + PointLight( + color="#A855F7", # Purple accent + intensity=0.6, + position={"x": 5, "y": 3, "z": 5}, + ), + ] + + +@dataclass +class LightingConfig: + """Lighting configuration for the Matrix world.""" + + ambient_color: str = "#FFAA55" # Warm amber (Workshop warmth) + ambient_intensity: float = 0.5 + point_lights: list[PointLight] = field(default_factory=_default_point_lights_factory) + + @classmethod + def from_dict(cls, data: dict[str, Any] | None) -> "LightingConfig": + """Create a LightingConfig from a dictionary with defaults.""" + if data is None: + data = {} + + point_lights_data = data.get("point_lights", []) + point_lights = ( + [PointLight.from_dict(pl) for pl in point_lights_data] + if point_lights_data + else _default_point_lights_factory() + ) + + return cls( + ambient_color=data.get("ambient_color", "#FFAA55"), + ambient_intensity=data.get("ambient_intensity", 0.5), + point_lights=point_lights, + ) + + +@dataclass +class EnvironmentConfig: + """Environment settings for the Matrix world.""" + + rain_enabled: bool = False + starfield_enabled: bool = True + fog_color: str = "#0f0f23" + fog_density: float = 0.02 + + @classmethod + def from_dict(cls, data: dict[str, Any] | None) -> "EnvironmentConfig": + """Create an EnvironmentConfig from a dictionary with defaults.""" + if data is None: + data = {} + return cls( + rain_enabled=data.get("rain_enabled", False), + starfield_enabled=data.get("starfield_enabled", True), + fog_color=data.get("fog_color", "#0f0f23"), + fog_density=data.get("fog_density", 0.02), + ) + + +@dataclass +class FeaturesConfig: + """Feature toggles for the Matrix world.""" + + chat_enabled: bool = True + visitor_avatars: bool = True + pip_familiar: bool = True + workshop_portal: bool = True + + @classmethod + def from_dict(cls, data: dict[str, Any] | None) -> "FeaturesConfig": + """Create a FeaturesConfig from a dictionary with defaults.""" + if data is None: + data = {} + return cls( + chat_enabled=data.get("chat_enabled", True), + visitor_avatars=data.get("visitor_avatars", True), + pip_familiar=data.get("pip_familiar", True), + workshop_portal=data.get("workshop_portal", True), + ) + + +@dataclass +class AgentConfig: + """Configuration for a single Matrix agent.""" + + name: str = "" + role: str = "" + enabled: bool = True + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "AgentConfig": + """Create an AgentConfig from a dictionary with defaults.""" + return cls( + name=data.get("name", ""), + role=data.get("role", ""), + enabled=data.get("enabled", True), + ) + + +@dataclass +class AgentsConfig: + """Agent registry configuration.""" + + default_count: int = 5 + max_count: int = 20 + agents: list[AgentConfig] = field(default_factory=list) + + @classmethod + def from_dict(cls, data: dict[str, Any] | None) -> "AgentsConfig": + """Create an AgentsConfig from a dictionary with defaults.""" + if data is None: + data = {} + + agents_data = data.get("agents", []) + agents = [AgentConfig.from_dict(a) for a in agents_data] if agents_data else [] + + return cls( + default_count=data.get("default_count", 5), + max_count=data.get("max_count", 20), + agents=agents, + ) + + +@dataclass +class MatrixConfig: + """Complete Matrix world configuration. + + Combines lighting, environment, features, and agent settings + into a single configuration object. + """ + + lighting: LightingConfig = field(default_factory=LightingConfig) + environment: EnvironmentConfig = field(default_factory=EnvironmentConfig) + features: FeaturesConfig = field(default_factory=FeaturesConfig) + agents: AgentsConfig = field(default_factory=AgentsConfig) + + @classmethod + def from_dict(cls, data: dict[str, Any] | None) -> "MatrixConfig": + """Create a MatrixConfig from a dictionary with defaults for missing sections.""" + if data is None: + data = {} + return cls( + lighting=LightingConfig.from_dict(data.get("lighting")), + environment=EnvironmentConfig.from_dict(data.get("environment")), + features=FeaturesConfig.from_dict(data.get("features")), + agents=AgentsConfig.from_dict(data.get("agents")), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert the configuration to a plain dictionary.""" + return { + "lighting": { + "ambient_color": self.lighting.ambient_color, + "ambient_intensity": self.lighting.ambient_intensity, + "point_lights": [ + { + "color": pl.color, + "intensity": pl.intensity, + "position": pl.position, + } + for pl in self.lighting.point_lights + ], + }, + "environment": { + "rain_enabled": self.environment.rain_enabled, + "starfield_enabled": self.environment.starfield_enabled, + "fog_color": self.environment.fog_color, + "fog_density": self.environment.fog_density, + }, + "features": { + "chat_enabled": self.features.chat_enabled, + "visitor_avatars": self.features.visitor_avatars, + "pip_familiar": self.features.pip_familiar, + "workshop_portal": self.features.workshop_portal, + }, + "agents": { + "default_count": self.agents.default_count, + "max_count": self.agents.max_count, + "agents": [ + {"name": a.name, "role": a.role, "enabled": a.enabled} + for a in self.agents.agents + ], + }, + } + + +def load_from_yaml(path: str | Path) -> MatrixConfig: + """Load Matrix configuration from a YAML file. + + Missing keys are filled with sensible defaults. If the file + cannot be read or parsed, returns a fully default configuration. + + Args: + path: Path to the YAML configuration file. + + Returns: + A MatrixConfig instance with loaded or default values. + """ + path = Path(path) + + if not path.exists(): + logger.warning("Matrix config file not found: %s, using defaults", path) + return MatrixConfig() + + try: + with open(path, encoding="utf-8") as f: + raw_data = yaml.safe_load(f) + + if not isinstance(raw_data, dict): + logger.warning("Matrix config invalid format, using defaults") + return MatrixConfig() + + return MatrixConfig.from_dict(raw_data) + + except yaml.YAMLError as exc: + logger.warning("Matrix config YAML parse error: %s, using defaults", exc) + return MatrixConfig() + except OSError as exc: + logger.warning("Matrix config read error: %s, using defaults", exc) + return MatrixConfig() diff --git a/tests/unit/test_matrix_config.py b/tests/unit/test_matrix_config.py new file mode 100644 index 00000000..3c1a514c --- /dev/null +++ b/tests/unit/test_matrix_config.py @@ -0,0 +1,331 @@ +"""Tests for the matrix configuration loader utility.""" + +from pathlib import Path + +import pytest +import yaml + +from infrastructure.matrix_config import ( + AgentConfig, + AgentsConfig, + EnvironmentConfig, + FeaturesConfig, + LightingConfig, + MatrixConfig, + PointLight, + load_from_yaml, +) + + +class TestPointLight: + """Tests for PointLight dataclass.""" + + def test_default_values(self): + """PointLight has correct defaults.""" + pl = PointLight() + assert pl.color == "#FFFFFF" + assert pl.intensity == 1.0 + assert pl.position == {"x": 0, "y": 0, "z": 0} + + def test_from_dict_full(self): + """PointLight.from_dict loads all fields.""" + data = { + "color": "#FF0000", + "intensity": 2.5, + "position": {"x": 1, "y": 2, "z": 3}, + } + pl = PointLight.from_dict(data) + assert pl.color == "#FF0000" + assert pl.intensity == 2.5 + assert pl.position == {"x": 1, "y": 2, "z": 3} + + def test_from_dict_partial(self): + """PointLight.from_dict fills missing fields with defaults.""" + data = {"color": "#00FF00"} + pl = PointLight.from_dict(data) + assert pl.color == "#00FF00" + assert pl.intensity == 1.0 + assert pl.position == {"x": 0, "y": 0, "z": 0} + + +class TestLightingConfig: + """Tests for LightingConfig dataclass.""" + + def test_default_values(self): + """LightingConfig has correct Workshop+Matrix blend defaults.""" + cfg = LightingConfig() + assert cfg.ambient_color == "#FFAA55" # Warm amber (Workshop) + assert cfg.ambient_intensity == 0.5 + assert len(cfg.point_lights) == 3 + # First light is warm amber center + assert cfg.point_lights[0].color == "#FFAA55" + # Second light is cool blue (Matrix) + assert cfg.point_lights[1].color == "#3B82F6" + + def test_from_dict_full(self): + """LightingConfig.from_dict loads all fields.""" + data = { + "ambient_color": "#123456", + "ambient_intensity": 0.8, + "point_lights": [ + {"color": "#ABCDEF", "intensity": 1.5, "position": {"x": 1, "y": 1, "z": 1}} + ], + } + cfg = LightingConfig.from_dict(data) + assert cfg.ambient_color == "#123456" + assert cfg.ambient_intensity == 0.8 + assert len(cfg.point_lights) == 1 + assert cfg.point_lights[0].color == "#ABCDEF" + + def test_from_dict_empty_list_uses_defaults(self): + """Empty point_lights list triggers default lights.""" + data = {"ambient_color": "#000000", "point_lights": []} + cfg = LightingConfig.from_dict(data) + assert cfg.ambient_color == "#000000" + assert len(cfg.point_lights) == 3 # Default lights + + def test_from_dict_none(self): + """LightingConfig.from_dict handles None.""" + cfg = LightingConfig.from_dict(None) + assert cfg.ambient_color == "#FFAA55" + assert len(cfg.point_lights) == 3 + + +class TestEnvironmentConfig: + """Tests for EnvironmentConfig dataclass.""" + + def test_default_values(self): + """EnvironmentConfig has correct defaults.""" + cfg = EnvironmentConfig() + assert cfg.rain_enabled is False + assert cfg.starfield_enabled is True # Matrix starfield + assert cfg.fog_color == "#0f0f23" + assert cfg.fog_density == 0.02 + + def test_from_dict_full(self): + """EnvironmentConfig.from_dict loads all fields.""" + data = { + "rain_enabled": True, + "starfield_enabled": False, + "fog_color": "#FFFFFF", + "fog_density": 0.1, + } + cfg = EnvironmentConfig.from_dict(data) + assert cfg.rain_enabled is True + assert cfg.starfield_enabled is False + assert cfg.fog_color == "#FFFFFF" + assert cfg.fog_density == 0.1 + + def test_from_dict_partial(self): + """EnvironmentConfig.from_dict fills missing fields.""" + data = {"rain_enabled": True} + cfg = EnvironmentConfig.from_dict(data) + assert cfg.rain_enabled is True + assert cfg.starfield_enabled is True # Default + assert cfg.fog_color == "#0f0f23" + + +class TestFeaturesConfig: + """Tests for FeaturesConfig dataclass.""" + + def test_default_values_all_enabled(self): + """FeaturesConfig defaults to all features enabled.""" + cfg = FeaturesConfig() + assert cfg.chat_enabled is True + assert cfg.visitor_avatars is True + assert cfg.pip_familiar is True + assert cfg.workshop_portal is True + + def test_from_dict_full(self): + """FeaturesConfig.from_dict loads all fields.""" + data = { + "chat_enabled": False, + "visitor_avatars": False, + "pip_familiar": False, + "workshop_portal": False, + } + cfg = FeaturesConfig.from_dict(data) + assert cfg.chat_enabled is False + assert cfg.visitor_avatars is False + assert cfg.pip_familiar is False + assert cfg.workshop_portal is False + + def test_from_dict_partial(self): + """FeaturesConfig.from_dict fills missing fields.""" + data = {"chat_enabled": False} + cfg = FeaturesConfig.from_dict(data) + assert cfg.chat_enabled is False + assert cfg.visitor_avatars is True # Default + assert cfg.pip_familiar is True + assert cfg.workshop_portal is True + + +class TestAgentConfig: + """Tests for AgentConfig dataclass.""" + + def test_default_values(self): + """AgentConfig has correct defaults.""" + cfg = AgentConfig() + assert cfg.name == "" + assert cfg.role == "" + assert cfg.enabled is True + + def test_from_dict_full(self): + """AgentConfig.from_dict loads all fields.""" + data = {"name": "Timmy", "role": "guide", "enabled": False} + cfg = AgentConfig.from_dict(data) + assert cfg.name == "Timmy" + assert cfg.role == "guide" + assert cfg.enabled is False + + +class TestAgentsConfig: + """Tests for AgentsConfig dataclass.""" + + def test_default_values(self): + """AgentsConfig has correct defaults.""" + cfg = AgentsConfig() + assert cfg.default_count == 5 + assert cfg.max_count == 20 + assert cfg.agents == [] + + def test_from_dict_with_agents(self): + """AgentsConfig.from_dict loads agent list.""" + data = { + "default_count": 10, + "max_count": 50, + "agents": [ + {"name": "Timmy", "role": "guide", "enabled": True}, + {"name": "Helper", "role": "assistant"}, + ], + } + cfg = AgentsConfig.from_dict(data) + assert cfg.default_count == 10 + assert cfg.max_count == 50 + assert len(cfg.agents) == 2 + assert cfg.agents[0].name == "Timmy" + assert cfg.agents[1].enabled is True # Default + + +class TestMatrixConfig: + """Tests for MatrixConfig dataclass.""" + + def test_default_values(self): + """MatrixConfig has correct composite defaults.""" + cfg = MatrixConfig() + assert isinstance(cfg.lighting, LightingConfig) + assert isinstance(cfg.environment, EnvironmentConfig) + assert isinstance(cfg.features, FeaturesConfig) + assert isinstance(cfg.agents, AgentsConfig) + # Check the blend + assert cfg.lighting.ambient_color == "#FFAA55" + assert cfg.environment.starfield_enabled is True + assert cfg.features.chat_enabled is True + + def test_from_dict_full(self): + """MatrixConfig.from_dict loads all sections.""" + data = { + "lighting": {"ambient_color": "#000000"}, + "environment": {"rain_enabled": True}, + "features": {"chat_enabled": False}, + "agents": {"default_count": 3}, + } + cfg = MatrixConfig.from_dict(data) + assert cfg.lighting.ambient_color == "#000000" + assert cfg.environment.rain_enabled is True + assert cfg.features.chat_enabled is False + assert cfg.agents.default_count == 3 + + def test_from_dict_partial(self): + """MatrixConfig.from_dict fills missing sections with defaults.""" + data = {"lighting": {"ambient_color": "#111111"}} + cfg = MatrixConfig.from_dict(data) + assert cfg.lighting.ambient_color == "#111111" + assert cfg.environment.starfield_enabled is True # Default + assert cfg.features.pip_familiar is True # Default + + def test_from_dict_none(self): + """MatrixConfig.from_dict handles None.""" + cfg = MatrixConfig.from_dict(None) + assert cfg.lighting.ambient_color == "#FFAA55" + assert cfg.features.chat_enabled is True + + def test_to_dict_roundtrip(self): + """MatrixConfig.to_dict produces serializable output.""" + cfg = MatrixConfig() + data = cfg.to_dict() + assert isinstance(data, dict) + assert "lighting" in data + assert "environment" in data + assert "features" in data + assert "agents" in data + # Verify point lights are included + assert len(data["lighting"]["point_lights"]) == 3 + + +class TestLoadFromYaml: + """Tests for load_from_yaml function.""" + + def test_loads_valid_yaml(self, tmp_path: Path): + """load_from_yaml reads a valid YAML file.""" + config_path = tmp_path / "matrix.yaml" + data = { + "lighting": {"ambient_color": "#TEST11"}, + "features": {"chat_enabled": False}, + } + config_path.write_text(yaml.safe_dump(data)) + + cfg = load_from_yaml(config_path) + assert cfg.lighting.ambient_color == "#TEST11" + assert cfg.features.chat_enabled is False + + def test_missing_file_returns_defaults(self, tmp_path: Path): + """load_from_yaml returns defaults when file doesn't exist.""" + config_path = tmp_path / "nonexistent.yaml" + cfg = load_from_yaml(config_path) + assert cfg.lighting.ambient_color == "#FFAA55" + assert cfg.features.chat_enabled is True + + def test_empty_file_returns_defaults(self, tmp_path: Path): + """load_from_yaml returns defaults for empty file.""" + config_path = tmp_path / "empty.yaml" + config_path.write_text("") + cfg = load_from_yaml(config_path) + assert cfg.lighting.ambient_color == "#FFAA55" + + def test_invalid_yaml_returns_defaults(self, tmp_path: Path): + """load_from_yaml returns defaults for invalid YAML.""" + config_path = tmp_path / "invalid.yaml" + config_path.write_text("not: valid: yaml: [") + cfg = load_from_yaml(config_path) + assert cfg.lighting.ambient_color == "#FFAA55" + assert cfg.features.chat_enabled is True + + def test_non_dict_yaml_returns_defaults(self, tmp_path: Path): + """load_from_yaml returns defaults when YAML is not a dict.""" + config_path = tmp_path / "list.yaml" + config_path.write_text("- item1\n- item2") + cfg = load_from_yaml(config_path) + assert cfg.lighting.ambient_color == "#FFAA55" + + def test_loads_actual_config_file(self): + """load_from_yaml can load the project's config/matrix.yaml.""" + repo_root = Path(__file__).parent.parent.parent + config_path = repo_root / "config" / "matrix.yaml" + if not config_path.exists(): + pytest.skip("config/matrix.yaml not found") + + cfg = load_from_yaml(config_path) + # Verify it loaded with expected values + assert cfg.lighting.ambient_color == "#FFAA55" + assert len(cfg.lighting.point_lights) == 3 + assert cfg.environment.starfield_enabled is True + assert cfg.features.workshop_portal is True + + def test_str_path_accepted(self, tmp_path: Path): + """load_from_yaml accepts string path.""" + config_path = tmp_path / "matrix.yaml" + config_path.write_text(yaml.safe_dump({"lighting": {"ambient_intensity": 0.9}})) + + cfg = load_from_yaml(str(config_path)) + assert cfg.lighting.ambient_intensity == 0.9