Files
meeting-room/tests/test_config.py
vitya d0507b3b0c feat: extract config.py, refactor CLI to use event-driven engine, add deprecation shim
- config.py: extracted load_config(), find_workspace_config(),
  load_discussion_config() from cli.py; fixed Windows root detection
- cli.py: run_discussion() now creates DiscussionEngine + EventEmitter,
  subscribes to events with ANSI-color output (identical to before),
  delegates to engine.run()
- backends/custom/run.py: deprecation shim with DeprecationWarning,
  delegates to DiscussionEngine
- 16 new config tests, all 184 tests passing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 00:33:07 +03:00

263 lines
9.7 KiB
Python

"""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_<PROV>_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_<PROV>_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