diff --git a/node/sophia_governor_review_service.py b/node/sophia_governor_review_service.py index 70e0335c5..0bcfe2c11 100644 --- a/node/sophia_governor_review_service.py +++ b/node/sophia_governor_review_service.py @@ -159,6 +159,24 @@ def _normalize_limit(value: Any, default: int = 10, maximum: int = 100) -> int: return max(1, min(limit, maximum)) +def _normalize_maintenance_limit(data: Any, default: int = 25, maximum: int = 200) -> int: + value = data.get("limit", default) if isinstance(data, dict) else default + if isinstance(value, bool): + raise ValueError("limit must be an integer") + if isinstance(value, int): + limit = value + elif isinstance(value, str): + cleaned = value.strip() + if not re.fullmatch(r"[+-]?\d+", cleaned): + raise ValueError("limit must be an integer") + limit = int(cleaned) + else: + raise ValueError("limit must be an integer") + if limit < 1: + raise ValueError("limit must be at least 1") + return min(limit, maximum) + + def _is_authorized(req) -> bool: required_admin = os.getenv("RC_ADMIN_KEY", "").strip() if required_admin: @@ -645,7 +663,10 @@ def backfill_missing(): if not _is_authorized(request): return jsonify({"error": "Unauthorized -- admin key or bearer required"}), 401 data = request.get_json(silent=True) or {} - limit = data.get("limit", 25) if isinstance(data, dict) else 25 + try: + limit = _normalize_maintenance_limit(data) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 results = backfill_missing_reviews(limit=limit) return jsonify({"ok": True, "updated": results, "count": len(results)}) @@ -656,7 +677,10 @@ def normalize_existing(): if not _is_authorized(request): return jsonify({"error": "Unauthorized -- admin key or bearer required"}), 401 data = request.get_json(silent=True) or {} - limit = data.get("limit", 25) if isinstance(data, dict) else 25 + try: + limit = _normalize_maintenance_limit(data) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 results = normalize_existing_reviews(limit=limit) return jsonify({"ok": True, "updated": results, "count": len(results)}) diff --git a/node/tests/test_sophia_governor_review_service.py b/node/tests/test_sophia_governor_review_service.py index fa238b428..505336e49 100644 --- a/node/tests/test_sophia_governor_review_service.py +++ b/node/tests/test_sophia_governor_review_service.py @@ -209,6 +209,36 @@ def test_backfill_missing_updates_blank_reviews(client, monkeypatch): assert "Assessment: repaired." in repaired["review_text"] +@pytest.mark.parametrize( + ("path", "limit", "error"), + [ + ("/api/sophia/governor/review/backfill-missing", "abc", "limit must be an integer"), + ("/api/sophia/governor/review/backfill-missing", "10.5", "limit must be an integer"), + ("/api/sophia/governor/review/backfill-missing", 10.5, "limit must be an integer"), + ("/api/sophia/governor/review/backfill-missing", True, "limit must be an integer"), + ("/api/sophia/governor/review/backfill-missing", False, "limit must be an integer"), + ("/api/sophia/governor/review/backfill-missing", 0, "limit must be at least 1"), + ("/api/sophia/governor/review/backfill-missing", -1, "limit must be at least 1"), + ("/api/sophia/governor/review/normalize-existing", "abc", "limit must be an integer"), + ("/api/sophia/governor/review/normalize-existing", "10.5", "limit must be an integer"), + ("/api/sophia/governor/review/normalize-existing", 10.5, "limit must be an integer"), + ("/api/sophia/governor/review/normalize-existing", True, "limit must be an integer"), + ("/api/sophia/governor/review/normalize-existing", False, "limit must be an integer"), + ("/api/sophia/governor/review/normalize-existing", 0, "limit must be at least 1"), + ("/api/sophia/governor/review/normalize-existing", -1, "limit must be at least 1"), + ], +) +def test_maintenance_routes_reject_invalid_limits(client, path, limit, error): + response = client.post( + path, + headers={"X-Admin-Key": "test-admin"}, + json={"limit": limit}, + ) + + assert response.status_code == 400 + assert response.get_json()["error"] == error + + def test_review_normalizes_verbose_action_reasoning(client, monkeypatch): monkeypatch.setattr( review_service,