From 9d4ac8e7ccb3fe05dcf8fadd5799ae1b5ad92f40 Mon Sep 17 00:00:00 2001 From: Kimi Agent Date: Sat, 21 Mar 2026 14:25:19 +0000 Subject: [PATCH] [kimi] Add /api/matrix/config endpoint for world configuration (#674) (#736) --- config/matrix.yaml | 28 +++++ src/dashboard/routes/world.py | 99 +++++++++++++++ tests/dashboard/test_world_api.py | 194 ++++++++++++++++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 config/matrix.yaml diff --git a/config/matrix.yaml b/config/matrix.yaml new file mode 100644 index 00000000..684df51b --- /dev/null +++ b/config/matrix.yaml @@ -0,0 +1,28 @@ +# Matrix World Configuration +# Serves lighting, environment, and feature settings to the Matrix frontend. + +lighting: + ambient_color: "#1a1a2e" + ambient_intensity: 0.4 + point_lights: + - color: "#FFD700" + intensity: 1.2 + position: { x: 0, y: 5, z: 0 } + - color: "#3B82F6" + intensity: 0.8 + position: { x: -5, y: 3, z: -5 } + - color: "#A855F7" + intensity: 0.6 + position: { x: 5, y: 3, z: 5 } + +environment: + rain_enabled: false + starfield_enabled: true + fog_color: "#0f0f23" + fog_density: 0.02 + +features: + chat_enabled: true + visitor_avatars: true + pip_familiar: true + workshop_portal: true diff --git a/src/dashboard/routes/world.py b/src/dashboard/routes/world.py index ded6ff9c..6863c141 100644 --- a/src/dashboard/routes/world.py +++ b/src/dashboard/routes/world.py @@ -22,11 +22,14 @@ import re import time from collections import deque from datetime import UTC, datetime +from pathlib import Path from typing import Any +import yaml from fastapi import APIRouter, WebSocket from fastapi.responses import JSONResponse +from config import settings from infrastructure.presence import serialize_presence from timmy.workshop_state import PRESENCE_FILE @@ -489,6 +492,102 @@ async def _generate_bark(visitor_text: str) -> str: return "Hmm, my thoughts are a bit tangled right now." +# --------------------------------------------------------------------------- +# Matrix Configuration Endpoint +# --------------------------------------------------------------------------- + +# Default Matrix configuration (fallback when matrix.yaml is missing/corrupt) +_DEFAULT_MATRIX_CONFIG: dict[str, Any] = { + "lighting": { + "ambient_color": "#1a1a2e", + "ambient_intensity": 0.4, + "point_lights": [ + {"color": "#FFD700", "intensity": 1.2, "position": {"x": 0, "y": 5, "z": 0}}, + {"color": "#3B82F6", "intensity": 0.8, "position": {"x": -5, "y": 3, "z": -5}}, + {"color": "#A855F7", "intensity": 0.6, "position": {"x": 5, "y": 3, "z": 5}}, + ], + }, + "environment": { + "rain_enabled": False, + "starfield_enabled": True, + "fog_color": "#0f0f23", + "fog_density": 0.02, + }, + "features": { + "chat_enabled": True, + "visitor_avatars": True, + "pip_familiar": True, + "workshop_portal": True, + }, +} + + +def _load_matrix_config() -> dict[str, Any]: + """Load Matrix world configuration from matrix.yaml with fallback to defaults. + + Returns a dict with sections: lighting, environment, features. + If the config file is missing or invalid, returns sensible defaults. + """ + try: + config_path = Path(settings.repo_root) / "config" / "matrix.yaml" + if not config_path.exists(): + logger.debug("matrix.yaml not found, using default config") + return _DEFAULT_MATRIX_CONFIG.copy() + + raw = config_path.read_text() + config = yaml.safe_load(raw) + if not isinstance(config, dict): + logger.warning("matrix.yaml invalid format, using defaults") + return _DEFAULT_MATRIX_CONFIG.copy() + + # Merge with defaults to ensure all required fields exist + result: dict[str, Any] = { + "lighting": { + **_DEFAULT_MATRIX_CONFIG["lighting"], + **config.get("lighting", {}), + }, + "environment": { + **_DEFAULT_MATRIX_CONFIG["environment"], + **config.get("environment", {}), + }, + "features": { + **_DEFAULT_MATRIX_CONFIG["features"], + **config.get("features", {}), + }, + } + + # Ensure point_lights is a list + if "point_lights" in config.get("lighting", {}): + result["lighting"]["point_lights"] = config["lighting"]["point_lights"] + else: + result["lighting"]["point_lights"] = _DEFAULT_MATRIX_CONFIG["lighting"]["point_lights"] + + return result + except Exception as exc: + logger.warning("Failed to load matrix config: %s, using defaults", exc) + return _DEFAULT_MATRIX_CONFIG.copy() + + +@matrix_router.get("/config") +async def get_matrix_config() -> JSONResponse: + """Return Matrix world configuration. + + Serves lighting presets, environment settings, and feature flags + to the Matrix frontend so it can be config-driven rather than + hardcoded. Reads from config/matrix.yaml with sensible defaults. + + Response structure: + - lighting: ambient_color, ambient_intensity, point_lights[] + - environment: rain_enabled, starfield_enabled, fog_color, fog_density + - features: chat_enabled, visitor_avatars, pip_familiar, workshop_portal + """ + config = _load_matrix_config() + return JSONResponse( + content=config, + headers={"Cache-Control": "no-cache, no-store"}, + ) + + # --------------------------------------------------------------------------- # Matrix Agent Registry Endpoint # --------------------------------------------------------------------------- diff --git a/tests/dashboard/test_world_api.py b/tests/dashboard/test_world_api.py index 9f0abfb8..82cf1f12 100644 --- a/tests/dashboard/test_world_api.py +++ b/tests/dashboard/test_world_api.py @@ -863,3 +863,197 @@ def test_matrix_agents_endpoint_graceful_degradation(matrix_client): assert resp.status_code == 200 assert resp.json() == [] + + +# --------------------------------------------------------------------------- +# Matrix Configuration Endpoint (/api/matrix/config) +# --------------------------------------------------------------------------- + + +class TestMatrixConfigEndpoint: + """Tests for the Matrix configuration endpoint.""" + + def test_matrix_config_endpoint_returns_json(self, matrix_client): + """GET /api/matrix/config returns JSON config.""" + resp = matrix_client.get("/api/matrix/config") + + assert resp.status_code == 200 + data = resp.json() + assert isinstance(data, dict) + assert "lighting" in data + assert "environment" in data + assert "features" in data + assert resp.headers["cache-control"] == "no-cache, no-store" + + def test_matrix_config_lighting_structure(self, matrix_client): + """Config has correct lighting section structure.""" + resp = matrix_client.get("/api/matrix/config") + data = resp.json() + + lighting = data["lighting"] + assert "ambient_color" in lighting + assert "ambient_intensity" in lighting + assert "point_lights" in lighting + assert isinstance(lighting["point_lights"], list) + + # Check first point light structure + if lighting["point_lights"]: + pl = lighting["point_lights"][0] + assert "color" in pl + assert "intensity" in pl + assert "position" in pl + assert "x" in pl["position"] + assert "y" in pl["position"] + assert "z" in pl["position"] + + def test_matrix_config_environment_structure(self, matrix_client): + """Config has correct environment section structure.""" + resp = matrix_client.get("/api/matrix/config") + data = resp.json() + + env = data["environment"] + assert "rain_enabled" in env + assert "starfield_enabled" in env + assert "fog_color" in env + assert "fog_density" in env + assert isinstance(env["rain_enabled"], bool) + assert isinstance(env["starfield_enabled"], bool) + + def test_matrix_config_features_structure(self, matrix_client): + """Config has correct features section structure.""" + resp = matrix_client.get("/api/matrix/config") + data = resp.json() + + features = data["features"] + assert "chat_enabled" in features + assert "visitor_avatars" in features + assert "pip_familiar" in features + assert "workshop_portal" in features + assert isinstance(features["chat_enabled"], bool) + + +class TestMatrixConfigLoading: + """Tests for _load_matrix_config function.""" + + def test_load_matrix_config_returns_dict(self): + """_load_matrix_config returns a dictionary.""" + from dashboard.routes.world import _load_matrix_config + + config = _load_matrix_config() + assert isinstance(config, dict) + assert "lighting" in config + assert "environment" in config + assert "features" in config + + def test_load_matrix_config_has_all_required_sections(self): + """Config contains all required sections.""" + from dashboard.routes.world import _load_matrix_config + + config = _load_matrix_config() + lighting = config["lighting"] + env = config["environment"] + features = config["features"] + + # Lighting fields + assert "ambient_color" in lighting + assert "ambient_intensity" in lighting + assert "point_lights" in lighting + + # Environment fields + assert "rain_enabled" in env + assert "starfield_enabled" in env + assert "fog_color" in env + assert "fog_density" in env + + # Features fields + assert "chat_enabled" in features + assert "visitor_avatars" in features + assert "pip_familiar" in features + assert "workshop_portal" in features + + def test_load_matrix_config_fallback_on_missing_file(self, tmp_path): + """Returns defaults when matrix.yaml is missing.""" + from dashboard.routes.world import _load_matrix_config + + with patch("dashboard.routes.world.settings") as mock_settings: + mock_settings.repo_root = str(tmp_path) + config = _load_matrix_config() + + # Should return defaults + assert config["lighting"]["ambient_color"] == "#1a1a2e" + assert config["environment"]["rain_enabled"] is False + assert config["features"]["chat_enabled"] is True + + def test_load_matrix_config_merges_with_defaults(self, tmp_path): + """Partial config file is merged with defaults.""" + from dashboard.routes.world import _load_matrix_config + + # Create a partial config file + config_dir = tmp_path / "config" + config_dir.mkdir() + config_file = config_dir / "matrix.yaml" + config_file.write_text(""" +lighting: + ambient_color: "#ff0000" + ambient_intensity: 0.8 +environment: + rain_enabled: true +""") + + with patch("dashboard.routes.world.settings") as mock_settings: + mock_settings.repo_root = str(tmp_path) + config = _load_matrix_config() + + # Custom values + assert config["lighting"]["ambient_color"] == "#ff0000" + assert config["lighting"]["ambient_intensity"] == 0.8 + assert config["environment"]["rain_enabled"] is True + + # Defaults preserved + assert config["features"]["chat_enabled"] is True + assert config["environment"]["starfield_enabled"] is True + assert len(config["lighting"]["point_lights"]) == 3 + + def test_load_matrix_config_handles_invalid_yaml(self, tmp_path): + """Returns defaults when YAML is invalid.""" + from dashboard.routes.world import _load_matrix_config + + config_dir = tmp_path / "config" + config_dir.mkdir() + config_file = config_dir / "matrix.yaml" + config_file.write_text("not: valid: yaml: [{") + + with patch("dashboard.routes.world.settings") as mock_settings: + mock_settings.repo_root = str(tmp_path) + config = _load_matrix_config() + + # Should return defaults despite invalid YAML + assert "lighting" in config + assert "environment" in config + assert "features" in config + + def test_load_matrix_config_custom_point_lights(self, tmp_path): + """Custom point lights override defaults completely.""" + from dashboard.routes.world import _load_matrix_config + + config_dir = tmp_path / "config" + config_dir.mkdir() + config_file = config_dir / "matrix.yaml" + config_file.write_text(""" +lighting: + point_lights: + - color: "#FFFFFF" + intensity: 2.0 + position: { x: 1, y: 2, z: 3 } +""") + + with patch("dashboard.routes.world.settings") as mock_settings: + mock_settings.repo_root = str(tmp_path) + config = _load_matrix_config() + + # Should have custom point lights, not defaults + lights = config["lighting"]["point_lights"] + assert len(lights) == 1 + assert lights[0]["color"] == "#FFFFFF" + assert lights[0]["intensity"] == 2.0 + assert lights[0]["position"] == {"x": 1, "y": 2, "z": 3}