Files
roboco/tests/unit/services/test_feature_flags.py
T
Renn F ce4d02b3c2 fix(gateway): bounded fail-open evidence assembly + PM decision transient-failure bypass
Evidence-assembly git legs (diff, changed-files, branch fetch, advisory
conventions run) ran unbounded inside claim_review / claim_doc_task /
claim_gate_review / evidence() / i_am_done's envelope build, so a slow
clone turned the whole verb into a silent 120s FlowVerbTimeout 504.
Each leg now runs through run_bounded_leg under a shared LegBudget
(evidence_assembly_timeout_seconds, 45s total): a timed-out leg — both
asyncio TimeoutError and git's own GitTimeoutError — degrades into an
evidence_gaps note on the envelope instead of hanging the verb, while
non-timeout git errors still propagate. The advisory conventions run
gets an inner-only timeout (conventions_validator_advisory_timeout_
seconds, 30s) threaded down to the subprocess so it is never orphaned
by an outer cancel; the fail-closed i_am_done/pr_pass conventions
gates keep their hardcoded 120s.

_ensure_pm_decision now reports a PmDecisionOutcome: a transient DB
failure recording the PM's decision journal (e.g. lock timeout under
load) no longer launders into a journal:decision gate rejection that
escalates and BLOCKS the task — the verb's own rationale satisfies the
gate with a structured warning, across all seven PM verbs.

Also: repo-wide ruff realignment to the lockfile-pinned ruff (8
format-only diffs, 14 UP038 isinstance conversions) that a transiently
newer venv ruff had masked.

Gate: 15474 passed, 459 skipped; ruff/mypy/xenon/vulture/bandit/
pip-audit/deptry/import-linter/foundation-check all green.
2026-07-31 13:56:19 +02:00

83 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