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"]