mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""Panel-tunable feature flags — validation, bool read, and startup overlay (#9).
|
|||
|
|
|
||
|
|
Flags persist in system_settings as 'true'/'false' and are overlaid onto the
|
||
|
|
config singleton at startup; an unset flag keeps its env/config default.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from roboco.config import settings as cfg
|
||
|
|
from roboco.services import settings as settings_mod
|
||
|
|
from roboco.services.settings import (
|
||
|
|
SettingsService,
|
||
|
|
SettingValidationError,
|
||
|
|
validate_setting,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_feature_flags_are_writable_as_bool() -> None:
|
||
|
|
validate_setting("external_pr_enabled", "true")
|
||
|
|
validate_setting("research_enabled", "FALSE") # case-insensitive
|
||
|
|
validate_setting("internal_pr_enabled", " true ") # whitespace tolerated
|
||
|
|
|
||
|
|
|
||
|
|
def test_non_bool_value_rejected() -> None:
|
||
|
|
with pytest.raises(SettingValidationError):
|
||
|
|
validate_setting("external_pr_enabled", "yes")
|
||
|
|
|
||
|
|
|
||
|
|
def test_unknown_key_rejected() -> None:
|
||
|
|
with pytest.raises(SettingValidationError):
|
||
|
|
validate_setting("not_a_real_flag", "true")
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_get_bool_parses_and_defaults() -> None:
|
||
|
|
svc = SettingsService(MagicMock())
|
||
|
|
object.__setattr__(svc, "get", AsyncMock(return_value="true"))
|
||
|
|
assert await svc.get_bool("k", default=False) is True
|
||
|
|
object.__setattr__(svc, "get", AsyncMock(return_value="false"))
|
||
|
|
assert await svc.get_bool("k", default=True) is False
|
||
|
|
object.__setattr__(svc, "get", AsyncMock(return_value=None))
|
||
|
|
assert await svc.get_bool("k", default=True) is True # unset → default
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_apply_overrides_stored_flags_only(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
|
||
|
|
# Baseline env defaults.
|
||
|
|
monkeypatch.setattr(cfg, "external_pr_enabled", False)
|
||
|
|
monkeypatch.setattr(cfg, "research_enabled", True)
|
||
|
|
|
||
|
|
# Only external_pr_enabled has a stored override; research_enabled is unset.
|
||
|
|
stored = {"external_pr_enabled": "true"}
|
||
|
|
|
||
|
|
async def fake_get(_self: SettingsService, key: str) -> str | None:
|
||
|
|
return stored.get(key)
|
||
|
|
|
||
|
|
monkeypatch.setattr(SettingsService, "get", fake_get)
|
||
|
|
applied = await settings_mod.apply_persisted_feature_flags(MagicMock())
|
||
|
|
|
||
|
|
assert "external_pr_enabled" in applied
|
||
|
|
assert cfg.external_pr_enabled is True # stored override applied
|
||
|
|
assert cfg.research_enabled is True # unset → env default untouched
|
||
|
|
assert "research_enabled" not in applied
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_effective_values_use_env_default_when_unset(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
|
||
|
|
monkeypatch.setattr(cfg, "strategy_engine_enabled", True)
|
||
|
|
|
||
|
|
async def fake_get(_self: SettingsService, _key: str) -> str | None:
|
||
|
|
return None # nothing stored
|
||
|
|
|
||
|
|
monkeypatch.setattr(SettingsService, "get", fake_get)
|
||
|
|
effective = await settings_mod.feature_flag_effective_values(MagicMock())
|
||
|
|
assert effective["strategy_engine_enabled"] is True # falls back to env
|