mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Add a Feature Flags card to the Settings page that toggles env-gated subsystems (external/internal PR review, web research, strategy engine, pitch provisioning, RAG auto-update, transcript pruning) directly from the panel instead of hand-editing environment variables. Flags persist in system_settings as 'true'/'false' and are overlaid onto the live config singleton at startup; an unset flag keeps its environment/config default. A toggle takes effect on the next backend restart — no per-consumer re-routing. Backend: FEATURE_FLAGS registry + bool validator + get_bool accessor on SettingsService; feature_flag_effective_values and apply_persisted_feature_flags; GET /settings/feature-flags; best-effort startup overlay in the app lifespan. Frontend: settingsApi.getFeatureFlags / setFeatureFlag and a FeatureFlagsCard rendered full-width below the settings grid.
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
|