Fix caller lineup never loading and add Wellspring cult callers
The lineup fix and the new characters both land in main.py, so they share a commit rather than being split artificially. Lineup fix — populate_backgrounds() was reachable only through POST /api/session/reset, so taking calls without hitting reset left session.caller_backgrounds empty for a whole show. Every caller got a hollow prompt, collapsed into generic relationship filler, and Silas lost his lore. Now start_call populates on demand, get_caller_prompt raises EmptyCallerBackgroundError instead of emitting an empty prompt, /api/callers reports lineup readiness to a header badge, and cross-episode topic dedup widened from 2 shows to 10. Wellspring callers — Doyle as the anchor defector plus a six-member pool, grouped by new frontmatter fields (group, group_lead, group_weight, register, explicitness). A faction is capped at one caller per show, with a 15% roll for two pairing the lead with one other member. group_weight lets an anchor carrying an arc get slots faster than texture characters. Also detects model refusals, which arrive as ordinary 200s and previously reached air as broken-character text or dead silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""Regression guard: a show once ran with session.caller_backgrounds empty because
|
||||
populate_backgrounds() was reachable only via POST /api/session/reset. Every caller
|
||||
got a hollow prompt and collapsed into generic relationship filler.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
import backend.main as m
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def quiet_call(monkeypatch):
|
||||
monkeypatch.setattr(m.audio_service, "stop_caller_audio", lambda *a, **k: None)
|
||||
monkeypatch.setattr(m, "_maybe_generate_callback", lambda: None)
|
||||
monkeypatch.setattr(m, "_enrich_background_async", lambda key: asyncio.sleep(0))
|
||||
monkeypatch.setattr(m.session, "intern_monitoring", False)
|
||||
m.session.current_caller_key = None
|
||||
yield
|
||||
# asyncio.run() closes the loop it creates and clears the thread's current
|
||||
# loop. tests/test_caller_service.py still calls asyncio.get_event_loop(),
|
||||
# which then raises — and this module sorts ahead of it. Leave a usable loop.
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
|
||||
|
||||
def test_start_call_populates_empty_lineup(monkeypatch, quiet_call):
|
||||
key = next(iter(m.CALLER_BASES))
|
||||
m.session.caller_backgrounds = {}
|
||||
called = []
|
||||
|
||||
async def fake_populate():
|
||||
called.append(True)
|
||||
m.session.caller_backgrounds = {
|
||||
key: {"name": "Silas", "voice": "Sebastian", "identity": "commune founder",
|
||||
"situation": "the convoy stopped", "specific_details": []}
|
||||
}
|
||||
|
||||
monkeypatch.setattr(m.session, "populate_backgrounds", fake_populate)
|
||||
asyncio.run(m.start_call(key))
|
||||
|
||||
assert called, "start_call must populate backgrounds when the lineup is empty"
|
||||
assert m.session.caller_backgrounds, "lineup should be populated after the call starts"
|
||||
|
||||
|
||||
def test_start_call_does_not_repopulate_existing_lineup(monkeypatch, quiet_call):
|
||||
key = next(iter(m.CALLER_BASES))
|
||||
m.session.caller_backgrounds = {
|
||||
key: {"name": "Silas", "voice": "Sebastian", "identity": "commune founder",
|
||||
"situation": "the convoy stopped", "specific_details": []}
|
||||
}
|
||||
called = []
|
||||
|
||||
async def fake_populate():
|
||||
called.append(True)
|
||||
|
||||
monkeypatch.setattr(m.session, "populate_backgrounds", fake_populate)
|
||||
asyncio.run(m.start_call(key))
|
||||
|
||||
assert not called, "an existing lineup must not be regenerated mid-show"
|
||||
|
||||
|
||||
def test_prompt_from_populated_background_is_not_hollow():
|
||||
prompt = m.get_caller_prompt({
|
||||
"name": "Silas", "identity": "founder of The Wellspring",
|
||||
"situation": "the convoy stopped on the shoulder of 118",
|
||||
"reason_calling": "Priscilla will not come out of the trailer",
|
||||
"secret_want": "to hear he is not the reason she broke",
|
||||
"specific_details": ["livestock trailer", "nine years in"],
|
||||
})
|
||||
assert "Silas" in prompt
|
||||
assert "Wellspring" in prompt
|
||||
assert "You are . " not in prompt
|
||||
Reference in New Issue
Block a user