Files
DECNET/tests/api/realism/test_config_api.py
anti 2cc60bd677 feat(realism): operator-tunable planner weights via realism_config
New realism_config table (uuid PK + unique key) + two repo methods
(get/set) backs an admin-only GET/PUT /api/v1/realism/config surface.

The planner now exposes apply_payload(payload) / current_payload() /
reset_to_defaults() and reads its weights through mutable module
globals; pick() resolves the live values each call. Validation
catches negative weights, zero totals, out-of-range canary_probability,
unknown content_class names, and silently drops cross-list entries
(canary class on the user list, etc).

The orchestrator worker calls _refresh_realism_config(repo) on
startup and every 5 ticks (~5min at 60s interval). Operator changes
land within one refresh window with no bus signal — the simpler path
for a knob whose latency tolerance is minutes.
2026-04-27 18:00:08 -04:00

110 lines
3.6 KiB
Python

"""GET/PUT /api/v1/realism/config — operator-tunable weights."""
from __future__ import annotations
import json
from unittest.mock import AsyncMock, patch
import pytest
from fastapi import HTTPException
from decnet.realism import planner
@pytest.fixture(autouse=True)
def _reset_planner():
yield
planner.reset_to_defaults()
@pytest.mark.asyncio
async def test_get_returns_defaults_when_no_row():
from decnet.web.router.realism.api_config import get_config
with patch("decnet.web.router.realism.api_config.repo") as mock_repo:
mock_repo.get_realism_config = AsyncMock(return_value=None)
result = await get_config(user={"uuid": "u", "role": "viewer"})
assert result["canary_probability"] == pytest.approx(0.03)
assert result["user_class_weights"]
@pytest.mark.asyncio
async def test_get_hydrates_from_db_row():
from decnet.web.router.realism.api_config import get_config
stored = json.dumps({"canary_probability": 0.10})
with patch("decnet.web.router.realism.api_config.repo") as mock_repo:
mock_repo.get_realism_config = AsyncMock(
return_value={"key": "weights", "value": stored},
)
result = await get_config(user={"uuid": "u", "role": "viewer"})
assert result["canary_probability"] == pytest.approx(0.10)
@pytest.mark.asyncio
async def test_get_serves_defaults_when_stored_payload_invalid():
"""Stored JSON parsed but failed planner validation: log + serve
defaults rather than 500."""
from decnet.web.router.realism.api_config import get_config
stored = json.dumps({"canary_probability": 9.0})
with patch("decnet.web.router.realism.api_config.repo") as mock_repo:
mock_repo.get_realism_config = AsyncMock(
return_value={"key": "weights", "value": stored},
)
result = await get_config(user={"uuid": "u", "role": "viewer"})
assert result["canary_probability"] == pytest.approx(0.03)
@pytest.mark.asyncio
async def test_put_persists_and_returns_snapshot():
from decnet.web.router.realism.api_config import put_config
with patch("decnet.web.router.realism.api_config.repo") as mock_repo:
mock_repo.set_realism_config = AsyncMock()
result = await put_config(
body={"canary_probability": 0.20},
user={"uuid": "u", "role": "admin", "username": "anti"},
)
assert result["canary_probability"] == pytest.approx(0.20)
mock_repo.set_realism_config.assert_awaited_once()
args, _ = mock_repo.set_realism_config.call_args
assert args[0] == "weights"
persisted = json.loads(args[1])
assert persisted["canary_probability"] == pytest.approx(0.20)
@pytest.mark.asyncio
async def test_put_returns_400_on_invalid_payload():
from decnet.web.router.realism.api_config import put_config
with patch("decnet.web.router.realism.api_config.repo") as mock_repo:
mock_repo.set_realism_config = AsyncMock()
with pytest.raises(HTTPException) as exc:
await put_config(
body={"canary_probability": 9.0},
user={"uuid": "u", "role": "admin", "username": "anti"},
)
assert exc.value.status_code == 400
# No DB write on validation failure.
mock_repo.set_realism_config.assert_not_called()
@pytest.mark.asyncio
async def test_put_rejects_non_dict_body():
from decnet.web.router.realism.api_config import put_config
with pytest.raises(HTTPException) as exc:
await put_config(
body=[1, 2, 3], # type: ignore[arg-type]
user={"uuid": "u", "role": "admin", "username": "anti"},
)
assert exc.value.status_code == 400