| """Loop-safety tests for the Fishbowl autoplay transport β no mocks, offline stub. |
| |
| Proves the keystone of the live-integration feature: autoplay can NEVER spin into an |
| infinite, token-burning loop. Three independent backstops are exercised against a real |
| ``FishbowlSession`` (deterministic stub, no API key): |
| |
| 1. a tripped governor budget stops autoplay (and never crashes the callback); |
| 2. a verdict at the head auto-pauses the show; |
| 3. a hard ``_MAX_AUTO_TICKS`` cap stops autoplay even with an unbounded budget. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from src.core.governor import BudgetExceeded, Governor |
| from src.ui.fishbowl import app as fb_app |
| from src.ui.fishbowl.app import advance_one_tick |
| from src.ui.fishbowl.session import FishbowlSession |
|
|
|
|
| def _session_with_governor(governor: Governor) -> FishbowlSession: |
| """A real session whose conductor uses *governor* (reset so genesis is written).""" |
| session = FishbowlSession("thousand-token-wood") |
| session.conductor.governor = governor |
| session.reset() |
| return session |
|
|
|
|
| |
|
|
|
|
| def test_advance_stops_on_budget_without_raising() -> None: |
| governor = Governor(max_total_calls=3) |
| session = _session_with_governor(governor) |
|
|
| k = session.head |
| ticks = 0 |
| stop_reason = None |
| |
| for _ in range(200): |
| k, ticks, stop_reason = advance_one_tick(session, k, ticks) |
| if stop_reason is not None: |
| break |
| else: |
| raise AssertionError("autoplay never stopped β infinite loop") |
|
|
| |
| |
| |
| |
| assert stop_reason |
| assert any(token in stop_reason.lower() for token in ("cap", "reached", "verdict", "max_")) |
|
|
|
|
| def test_advance_surfaces_budget_reason_at_head() -> None: |
| """When the governor is already exhausted at the head, the tick returns its reason.""" |
| session = FishbowlSession("thousand-token-wood") |
| session.reset() |
| assert not session.has_verdict() |
| |
| session.conductor.governor.max_total_calls = 0 |
| k, ticks, stop_reason = advance_one_tick(session, session.head, 0) |
| assert stop_reason is not None |
| |
| assert "max_total_calls" in stop_reason.lower() |
| assert "tick cap" not in stop_reason.lower() |
| assert ticks == 0 |
|
|
|
|
| def test_session_step_raises_budget_exceeded_when_exhausted() -> None: |
| """The raw transport surfaces BudgetExceeded; the UI handler is what swallows it.""" |
| governor = Governor(max_total_calls=2) |
| session = _session_with_governor(governor) |
| raised = False |
| for _ in range(200): |
| try: |
| session.step() |
| except BudgetExceeded: |
| raised = True |
| break |
| assert raised, "a bounded governor must eventually raise BudgetExceeded" |
|
|
|
|
| |
|
|
|
|
| def test_advance_stops_on_verdict() -> None: |
| |
| session = FishbowlSession("thousand-token-wood") |
| session.reset() |
| for _ in range(200): |
| if session.has_verdict(): |
| break |
| session.step() |
| if not session.has_verdict(): |
| |
| return |
| _, _, stop_reason = advance_one_tick(session, session.head, 0) |
| assert stop_reason is not None |
| assert "verdict" in stop_reason.lower() |
|
|
|
|
| |
|
|
|
|
| def test_advance_stops_at_tick_cap() -> None: |
| |
| session = _session_with_governor(Governor(max_total_calls=10_000, max_turns=10_000)) |
| |
| head_before = session.head |
| k, ticks, stop_reason = advance_one_tick(session, session.head, fb_app._MAX_AUTO_TICKS) |
| assert stop_reason is not None |
| assert str(fb_app._MAX_AUTO_TICKS) in stop_reason |
| assert session.head == head_before |
|
|
|
|
| def test_advance_counts_only_generating_ticks() -> None: |
| session = _session_with_governor(Governor(max_total_calls=10_000, max_turns=10_000)) |
| head = session.head |
| |
| _, ticks, stop_reason = advance_one_tick(session, head - 1, 5) |
| assert stop_reason is None |
| assert ticks == 5 |
| |
| _, ticks2, stop2 = advance_one_tick(session, head, 5) |
| assert stop2 is None |
| assert ticks2 == 6 |
|
|
|
|
| def test_autoplay_streams_one_agent_per_advance() -> None: |
| |
| |
| session = _session_with_governor(Governor(max_total_calls=10_000, max_turns=10_000)) |
| k = session.head |
| for _ in range(6): |
| before = session.head |
| k, _ticks, stop = advance_one_tick(session, k, 0) |
| if stop: |
| break |
| assert session.head - before <= 1 |
| assert k == session.head |
|
|
|
|
| |
|
|
|
|
| def test_stopped_banner_reuses_verdict_chrome() -> None: |
| html = fb_app._stopped_banner_html("Total call cap 3 reached") |
| assert "verdict banner" in html |
| assert "banner stopped" in html |
| assert "The show has ended" in html |
| assert "Total call cap 3 reached" in html |
|
|
|
|
| def test_topbar_chip_offline_by_default() -> None: |
| |
| from src.models import inference |
|
|
| chip = fb_app._live_chip() |
| assert "OFFLINE" in chip or "LIVE" in chip |
| if not inference.configured_backends(): |
| assert "OFFLINE" in chip and "STUB" in chip |
|
|