mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(settings): panel-tunable feature flags
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.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user