"""Tests for meeting_room.config — config loading and DiscussionConfig conversion.""" from __future__ import annotations import os import tempfile import pytest import yaml from meeting_room.config import ( find_workspace_config, load_config, load_discussion_config, ) from meeting_room.models import ( DefaultsConfig, DiscussionConfig, ProviderConfig, RoleConfig, ) # --------------------------------------------------------------------------- # load_config — basic loading # --------------------------------------------------------------------------- class TestLoadConfig: def test_loads_default_config(self) -> None: """load_config() with no path loads the package default config.yaml.""" config = load_config() assert "providers" in config assert "roles" in config assert "defaults" in config def test_loads_custom_config(self, tmp_path) -> None: """load_config(path) loads a specific YAML file.""" custom = { "providers": { "test": {"base_url": "https://api.test.com/v1", "api_key": "sk-test"}, }, "roles": { "analyst": { "name": "Analyst", "provider": "test", "model": "gpt-3.5", "temperature": 0.5, "tools": "none", "system_prompt": "You are an analyst.", }, }, "defaults": {"max_rounds": 5, "language": "en"}, } config_path = tmp_path / "config.yaml" config_path.write_text(yaml.dump(custom, default_flow_style=False), encoding="utf-8") config = load_config(str(config_path)) assert config["providers"]["test"]["base_url"] == "https://api.test.com/v1" assert config["roles"]["analyst"]["name"] == "Analyst" assert config["defaults"]["max_rounds"] == 5 def test_env_var_resolution(self, tmp_path, monkeypatch) -> None: """${ENV_VAR} placeholders are resolved from the environment.""" monkeypatch.setenv("TEST_MR_API_KEY", "sk-from-env") monkeypatch.setenv("TEST_MR_BASE_URL", "https://env-url.com/v1") custom = { "providers": { "testprov": { "base_url": "${TEST_MR_BASE_URL}", "api_key": "${TEST_MR_API_KEY}", }, }, "roles": {}, "defaults": {"max_rounds": 3}, } config_path = tmp_path / "config.yaml" config_path.write_text(yaml.dump(custom, default_flow_style=False), encoding="utf-8") config = load_config(str(config_path)) assert config["providers"]["testprov"]["base_url"] == "https://env-url.com/v1" assert config["providers"]["testprov"]["api_key"] == "sk-from-env" def test_unresolved_env_var_preserved(self, tmp_path) -> None: """Unresolved ${ENV_VAR} stays as-is in the config.""" custom = { "providers": { "testprov": { "base_url": "https://api.test.com/v1", "api_key": "${NONEXISTENT_VAR_12345}", }, }, "roles": {}, "defaults": {}, } config_path = tmp_path / "config.yaml" config_path.write_text(yaml.dump(custom, default_flow_style=False), encoding="utf-8") config = load_config(str(config_path)) assert config["providers"]["testprov"]["api_key"] == "${NONEXISTENT_VAR_12345}" def test_env_override_api_key(self, monkeypatch) -> None: """MEETING_ROOM__API_KEY overrides the config value.""" monkeypatch.setenv("MEETING_ROOM_ROUTERAI_API_KEY", "sk-override") config = load_config() assert config["providers"]["routerai"]["api_key"] == "sk-override" def test_env_override_base_url(self, monkeypatch) -> None: """MEETING_ROOM__BASE_URL overrides the config value.""" monkeypatch.setenv("MEETING_ROOM_ROUTERAI_BASE_URL", "https://override.com/v1") config = load_config() assert config["providers"]["routerai"]["base_url"] == "https://override.com/v1" # --------------------------------------------------------------------------- # find_workspace_config # --------------------------------------------------------------------------- class TestFindWorkspaceConfig: def test_finds_config_in_cwd(self, tmp_path, monkeypatch) -> None: """Finds .meeting-room/config.yaml in the current working directory.""" ws_dir = tmp_path / ".meeting-room" ws_dir.mkdir() (ws_dir / "config.yaml").write_text("defaults: {}", encoding="utf-8") monkeypatch.chdir(tmp_path) result = find_workspace_config() assert result is not None assert result.endswith(".meeting-room\\config.yaml") or result.endswith(".meeting-room/config.yaml") def test_finds_config_in_parent(self, tmp_path, monkeypatch) -> None: """Searches upward to find .meeting-room/config.yaml.""" ws_dir = tmp_path / ".meeting-room" ws_dir.mkdir() (ws_dir / "config.yaml").write_text("defaults: {}", encoding="utf-8") child = tmp_path / "subdir" child.mkdir() monkeypatch.chdir(child) result = find_workspace_config() assert result is not None def test_returns_none_when_not_found(self, tmp_path, monkeypatch) -> None: """Returns None when no .meeting-room/config.yaml exists.""" # Use a tmp dir with no .meeting-room isolated = tmp_path / "isolated" isolated.mkdir() monkeypatch.chdir(isolated) # Ensure no .meeting-room exists anywhere up to root # (This may find a .meeting-room in a parent if one exists, # but in a temp dir hierarchy it shouldn't) result = find_workspace_config() # We can't guarantee None since there might be .meeting-room in parent dirs, # so we just check it's either None or a valid path if result is not None: assert os.path.exists(result) # --------------------------------------------------------------------------- # load_discussion_config # --------------------------------------------------------------------------- class TestLoadDiscussionConfig: def test_returns_discussion_config(self) -> None: """load_discussion_config() returns a DiscussionConfig instance.""" config = load_discussion_config() assert isinstance(config, DiscussionConfig) assert isinstance(config.providers, dict) assert isinstance(config.roles, dict) assert isinstance(config.defaults, DefaultsConfig) def test_providers_are_typed(self) -> None: """Provider values are ProviderConfig instances.""" config = load_discussion_config() for name, prov in config.providers.items(): assert isinstance(prov, ProviderConfig), f"Provider {name} is not ProviderConfig" assert prov.base_url assert prov.api_key is not None def test_roles_are_typed(self) -> None: """Role values are RoleConfig instances.""" config = load_discussion_config() for name, role in config.roles.items(): assert isinstance(role, RoleConfig), f"Role {name} is not RoleConfig" assert role.name assert role.provider assert role.model def test_defaults_populated(self) -> None: """DefaultsConfig has expected fields.""" config = load_discussion_config() assert config.defaults.max_rounds > 0 assert config.defaults.language is not None assert config.defaults.framework is not None def test_custom_config_path(self, tmp_path) -> None: """load_discussion_config(path) loads from a custom file.""" custom = { "providers": { "test": {"base_url": "https://api.test.com/v1", "api_key": "sk-test"}, }, "roles": { "analyst": { "name": "Analyst", "provider": "test", "model": "gpt-3.5", "temperature": 0.5, "tools": "none", "system_prompt": "You are an analyst.", }, }, "defaults": {"max_rounds": 5, "language": "en"}, } config_path = tmp_path / "config.yaml" config_path.write_text(yaml.dump(custom, default_flow_style=False), encoding="utf-8") config = load_discussion_config(str(config_path)) assert isinstance(config, DiscussionConfig) assert "test" in config.providers assert config.providers["test"].base_url == "https://api.test.com/v1" assert "analyst" in config.roles assert config.roles["analyst"].model == "gpt-3.5" assert config.defaults.max_rounds == 5 def test_env_overrides_applied_before_typing(self, monkeypatch) -> None: """Environment variable overrides are applied before converting to typed config.""" monkeypatch.setenv("MEETING_ROOM_ROUTERAI_API_KEY", "sk-from-env") config = load_discussion_config() assert config.providers["routerai"].api_key == "sk-from-env" def test_minimal_config(self, tmp_path) -> None: """A minimal config with just defaults works.""" minimal = { "providers": {}, "roles": {}, "defaults": {"max_rounds": 3}, } config_path = tmp_path / "config.yaml" config_path.write_text(yaml.dump(minimal, default_flow_style=False), encoding="utf-8") config = load_discussion_config(str(config_path)) assert isinstance(config, DiscussionConfig) assert len(config.providers) == 0 assert len(config.roles) == 0 assert config.defaults.max_rounds == 3