Spaces:
Sleeping
Sleeping
File size: 5,636 Bytes
1ec996f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """Tests for the user-tunable repetition / failsafe limits exposed via
`ConversationLimits` and the `/api/chat/limits/defaults` endpoint.
These guard:
- defaults match the historical hard-coded values (no behavior change
for anyone who doesn't touch the new settings);
- clamping silently coerces missing / out-of-range / non-int values
back to the per-field defaults rather than raising;
- the defaults endpoint returns parallel `defaults` / `bounds` /
`descriptions` maps with the same field names so the frontend can
zip them into UI rows.
"""
from fastapi.testclient import TestClient
from app.main import app
from app.services.models import (
CONVERSATION_LIMIT_BOUNDS,
CONVERSATION_LIMIT_DESCRIPTIONS,
ConversationLimits,
clamp_conversation_limits,
)
# ---------------------------------------------------------------------------
# Defaults
# ---------------------------------------------------------------------------
def test_defaults_match_historical_hardcoded_values():
"""If anyone changes a default by accident, this test makes them
think twice. The values here are the ones the orchestrator used
before this knobs-as-settings refactor."""
limits = ConversationLimits()
assert limits.critique_rounds == 2
assert limits.status_assessment_max == 3
assert limits.consensus_turns_per_participant == 6
assert limits.dyad_cap == 2
assert limits.stall_recovery_attempts == 1
assert limits.auto_disable_failures == 3
assert limits.participant_message_pause_at == 60
assert limits.participant_message_pause_inc == 20
assert limits.orchestrator_call_pause_at == 100
assert limits.orchestrator_call_pause_inc == 50
def test_bounds_cover_every_dataclass_field():
"""Every tunable field needs a (min, max) bound; otherwise the
clamp helper would silently leave it unprotected."""
field_names = set(ConversationLimits().__dict__.keys())
bound_names = set(CONVERSATION_LIMIT_BOUNDS.keys())
assert field_names == bound_names
def test_descriptions_cover_every_dataclass_field():
"""The settings UI is server-driven; if a field has no description
block, the modal would render an empty row for it."""
field_names = set(ConversationLimits().__dict__.keys())
described = set(CONVERSATION_LIMIT_DESCRIPTIONS.keys())
assert field_names == described
for entry in CONVERSATION_LIMIT_DESCRIPTIONS.values():
assert "group" in entry
assert "label" in entry
assert "help" in entry
# ---------------------------------------------------------------------------
# clamp_conversation_limits
# ---------------------------------------------------------------------------
def test_clamp_returns_defaults_for_none_or_empty():
assert clamp_conversation_limits(None) == ConversationLimits()
assert clamp_conversation_limits({}) == ConversationLimits()
def test_clamp_clamps_too_large_values():
"""A value above the field's upper bound is coerced to the upper
bound, not rejected. This keeps the API permissive."""
out = clamp_conversation_limits({"critique_rounds": 99})
assert out.critique_rounds == CONVERSATION_LIMIT_BOUNDS["critique_rounds"][1]
def test_clamp_clamps_too_small_values():
out = clamp_conversation_limits({"critique_rounds": -10})
assert out.critique_rounds == CONVERSATION_LIMIT_BOUNDS["critique_rounds"][0]
def test_clamp_ignores_unknown_fields():
out = clamp_conversation_limits({"there_is_no_such_field": 7})
assert out == ConversationLimits()
def test_clamp_silently_drops_non_int_values():
"""Stringy garbage that can't be coerced should fall back to the
default for that one field, not raise."""
out = clamp_conversation_limits({"critique_rounds": "not-a-number"})
assert out.critique_rounds == ConversationLimits().critique_rounds
def test_clamp_preserves_partial_overrides():
"""Only-some-fields-supplied is the common case (UI sends just
the overridden ones)."""
out = clamp_conversation_limits({"dyad_cap": 4})
assert out.dyad_cap == 4
# All other fields untouched.
base = ConversationLimits()
for field_name in CONVERSATION_LIMIT_BOUNDS.keys():
if field_name == "dyad_cap":
continue
assert getattr(out, field_name) == getattr(base, field_name)
def test_clamp_accepts_string_ints():
"""JSON sometimes serializes numbers as strings - we should not
refuse a perfectly valid int just because it arrived as '4'."""
out = clamp_conversation_limits({"dyad_cap": "4"})
assert out.dyad_cap == 4
# ---------------------------------------------------------------------------
# /api/chat/limits/defaults endpoint
# ---------------------------------------------------------------------------
def test_limits_defaults_endpoint_returns_parallel_maps():
client = TestClient(app)
resp = client.get("/api/chat/limits/defaults")
assert resp.status_code == 200
body = resp.json()
assert set(body.keys()) == {"defaults", "bounds", "descriptions"}
# All three maps should agree on the same field set so the
# frontend can zip them per row.
field_names = set(ConversationLimits().__dict__.keys())
assert set(body["defaults"].keys()) == field_names
assert set(body["bounds"].keys()) == field_names
assert set(body["descriptions"].keys()) == field_names
# Bounds shape sanity-check.
for field_name, bound in body["bounds"].items():
assert "min" in bound and "max" in bound
assert bound["min"] < bound["max"]
assert bound["min"] <= body["defaults"][field_name] <= bound["max"]
|