nightwave / tests /test_proxy.py
ratandeep's picture
SP4: magical dial — off-dial AI ghost fragments buried in static
1743612 verified
Raw
History Blame Contribute Delete
16.3 kB
"""Tests for space/proxy.py turn logic (offline; mock mode + monkeypatch)."""
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_SPACE_DIR = os.path.dirname(_HERE)
if _SPACE_DIR not in sys.path:
sys.path.insert(0, _SPACE_DIR)
import arc # noqa: E402
import content # noqa: E402
import proxy # noqa: E402
def test_templated_rejoin_names_the_dj():
assert arc.DJ_NAME in proxy._templated_text("rejoin")
def test_templated_caller_intro_nonempty():
assert proxy._templated_text("caller_intro")
def test_song_intro_template_includes_handle():
t = proxy._templated_text(
"song_intro",
{"title": "Neon Rain", "artist": "The Tuesday Ghosts", "recommended_by": "@mara"},
)
assert "@mara" in t
def test_song_intro_template_omits_handle_when_absent():
t = proxy._templated_text(
"song_intro", {"title": "Neon Rain", "artist": "The Tuesday Ghosts"}
)
assert "@" not in t
def test_wmo_phrase_mapping():
assert proxy._wmo_phrase(0) == "clear skies"
assert proxy._wmo_phrase(61) == "light rain"
assert proxy._wmo_phrase(999) == "quiet skies" # unknown -> safe default
def test_fmt_clock():
assert proxy._fmt_clock("2026-06-15T02:14") == "2:14 AM"
assert proxy._fmt_clock("2026-06-15T13:05") == "1:05 PM"
assert proxy._fmt_clock(None) is None
def test_resolve_locale_mock(monkeypatch):
monkeypatch.setenv("NIGHTWAVE_MOCK", "1")
f = proxy.resolve_locale(30.27, -97.74)
assert f["resolved"] is True
assert f["city"] and f["temp_f"] is not None and f["sky"]
def test_local_weather_falls_back_to_fictional_on_junk(monkeypatch):
monkeypatch.setattr(proxy, "call_brain",
lambda system, messages: {"text": "<sentence 1>", "mood": "warm",
"arc_cue": "none"})
monkeypatch.setattr(proxy, "call_speak",
lambda text, voice=arc.VOICE: {"audio_b64": "", "words": [],
"wtimes": [], "wdurations": []})
out = proxy.segment_turn("local_weather", {"city": "Austin", "temp_f": 58})
assert out["text"] in content.WEATHER # junk brain text -> fictional fallback
assert out["kind"] == "local_weather"
# ---------------------------------------------------------------------------
# Regression coverage for the segment_turn junk-fallback path and the two
# segment helpers server.py calls (segment_fallback, get_stalls). `_clean_dj_text`
# is the junk-detector the Task-5 local_weather fallback test relies on; cover it
# directly so that load-bearing behaviour can't silently regress.
# ---------------------------------------------------------------------------
def test_clean_dj_text_rejects_degenerate_text_as_none():
# Empty / placeholder / angle-bracket template / too-few-letters -> None,
# so segment_turn falls back to a clean templated line instead of junk. This
# is the exact mechanism the Task-5 local_weather fallback test depends on.
assert proxy._clean_dj_text("") is None
assert proxy._clean_dj_text(None) is None
assert proxy._clean_dj_text("<sentence 1>") is None
assert proxy._clean_dj_text("a b c") is None
assert proxy._clean_dj_text("Sit tight, night owls.") == "Sit tight, night owls."
def test_get_stalls_returns_cached_clip_list(monkeypatch):
# server.py /api/stalls calls this; reset the module cache for order-independence.
monkeypatch.setattr(proxy, "_stall_cache", None)
clips = proxy.get_stalls()
assert isinstance(clips, list) and len(clips) == len(proxy._STALL_LINES)
assert all(isinstance(c, str) and c for c in clips) # each is a base64 WAV
assert proxy.get_stalls() is clips # second call is cached
def test_segment_fallback_is_never_failing_and_well_shaped():
# server.py /api/segment uses this on any failure so the show never breaks.
fb = proxy.segment_fallback("thought")
assert fb["kind"] == "thought"
assert fb["text"] in content.THOUGHTS # templated, stage-appropriate
assert fb["audio_b64"] # a real (silent) WAV bed
for key in ("mood", "arc_cue", "words", "wtimes", "wdurations"):
assert key in fb
# ---------------------------------------------------------------------------
# Regression (bug 2): the all-caps station name must be normalized before TTS so
# Kokoro speaks "Nightwave" instead of spelling N-I-G-H-T-W-A-V-E. _speakable was
# dropped during a proxy rebuild and call_speak sent raw text.
# ---------------------------------------------------------------------------
def test_speakable_titlecases_nightwave():
assert proxy._speakable("You're on NIGHTWAVE 98.6") == "You're on Nightwave 98.6"
assert proxy._speakable("night wave") == "Nightwave"
assert proxy._speakable(None) == ""
def test_call_speak_normalizes_nightwave_before_tts(monkeypatch):
sent = {}
class _Resp:
def raise_for_status(self):
pass
def json(self):
return {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []}
class _Client:
def post(self, url, headers=None, json=None):
sent.update(json or {})
return _Resp()
monkeypatch.setattr(proxy, "is_mock", lambda: False)
monkeypatch.setattr(proxy, "_modal_url", lambda: "http://modal.test")
monkeypatch.setattr(proxy, "_modal_headers", lambda: {})
monkeypatch.setattr(proxy, "_get_client", lambda: _Client())
proxy.call_speak("This is NIGHTWAVE, the last station on the dial.")
assert "NIGHTWAVE" not in sent["text"]
assert "Nightwave" in sent["text"]
# ---------------------------------------------------------------------------
# Regression (bug 4): the caller turn must ANSWER the caller, not echo. The
# straight station uses build_host_prompt("caller") + an answer-directive user
# turn so a 1B model doesn't parrot a question that appears verbatim in context.
# call_turn had reverted to the arc builder + raw caller_text as the user message.
# ---------------------------------------------------------------------------
def test_call_turn_answers_without_echoing_the_question(monkeypatch):
captured = {}
monkeypatch.setattr(proxy, "call_asr", lambda a: {"text": "are you real?"})
def _brain(system, messages):
captured["system"] = system
captured["messages"] = messages
return {"text": "Real as the rain on the glass, friend.", "mood": "warm", "arc_cue": "none"}
monkeypatch.setattr(proxy, "call_brain", _brain)
monkeypatch.setattr(proxy, "call_speak",
lambda text, voice=arc.VOICE: {"audio_b64": "", "words": [],
"wtimes": [], "wdurations": []})
out = proxy.call_turn("oblivious", 0, "<b64>")
# straight-station host prompt (names the DJ), NOT the arc builder
assert arc.DJ_NAME in captured["system"]
# the user turn carries an answer-directive, not the bare echoed question
user = captured["messages"][-1]["content"]
assert "do not repeat" in user.lower()
assert user.strip() != "are you real?"
assert out["caller_text"] == "are you real?"
assert out["meter_delta"] >= 14 # an identity/reality question pushes the meter
# ---------------------------------------------------------------------------
# Prompt-audit fixes: structured field labels (title:/artist:/temp:/...) must
# never reach TTS; degenerate/echo output is rejected -> templated fallback.
# ---------------------------------------------------------------------------
def test_clean_dj_text_rejects_label_leakage():
assert proxy._clean_dj_text("title: Neon Rain, artist: The Tuesday Ghosts") is None
assert proxy._clean_dj_text('{"text": "hi", "mood": "warm"}') is None
assert proxy._clean_dj_text("[song]") is None
assert proxy._clean_dj_text("temp: 58, sky: clear skies") is None
assert proxy._clean_dj_text("Coming up next, a quiet one for the night owls.") == \
"Coming up next, a quiet one for the night owls."
def test_call_turn_degenerate_uses_caller_fallback_not_crash(monkeypatch):
# The model leaks labels -> _clean_dj_text returns None -> must use a clean
# caller fallback line (the old arc.sample_line was undefined and crashed here).
monkeypatch.setattr(proxy, "call_asr", lambda a: {"text": "you there?"})
monkeypatch.setattr(proxy, "call_brain",
lambda s, m: {"text": "title: X, artist: Y", "mood": "warm", "arc_cue": "none"})
monkeypatch.setattr(proxy, "call_speak",
lambda text, voice=arc.VOICE: {"audio_b64": "", "words": [],
"wtimes": [], "wdurations": []})
out = proxy.call_turn("oblivious", 0, "<b64>")
assert out["text"] in content.CALLER_FALLBACKS
def test_broadcast_turn_uses_host_path_and_sanitizes(monkeypatch):
captured = {}
def _brain(system, messages):
captured["system"] = system
return {"text": "artist: X", "mood": "warm", "arc_cue": "none"} # degenerate
monkeypatch.setattr(proxy, "call_brain", _brain)
monkeypatch.setattr(proxy, "call_speak",
lambda text, voice=arc.VOICE: {"audio_b64": "", "words": [],
"wtimes": [], "wdurations": []})
out = proxy.broadcast_turn("oblivious", 0, None)
assert arc.DJ_NAME in captured["system"] # straight-station host, not arc builder
assert out["text"] in content.THOUGHTS # degenerate -> templated thought
assert out["arc_cue"] == "none"
def test_extract_name_place():
assert proxy._extract_name("Hi, I'm Mira and I'm calling in") == "Mira"
assert proxy._extract_name("just wanted to say hi") is None
assert proxy._extract_place("I'm driving in Austin tonight") == "Austin"
assert proxy._extract_place("no place here") is None
def test_caller_gist():
assert proxy._caller_gist(" I'm driving home after a double shift ") == \
"I'm driving home after a double shift"
assert proxy._caller_gist("hi") is None
assert proxy._caller_gist("") is None
def test_build_memory_patch():
p = proxy._build_memory_patch("I'm Mira, driving home after a double shift", "tender")
assert p["caller_name"] == "Mira" and p["topic"] and p["mood"] == "tender"
assert proxy._build_memory_patch("hi", "warm") is None # too short -> None
assert proxy._build_memory_patch("I'm driving all night", "bogus")["mood"] == "warm" # bad mood -> warm
def test_call_turn_returns_memory_patch(monkeypatch):
monkeypatch.setattr(proxy, "call_asr",
lambda a: {"text": "I'm Mira, driving home after a double shift"})
monkeypatch.setattr(proxy, "call_brain",
lambda s, m: {"text": "Drive safe, friend.", "mood": "tender", "arc_cue": "none"})
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
out = proxy.call_turn("oblivious", 0, "<b64>")
assert out["queue_dedication"] is True
assert out["memory_patch"]["caller_name"] == "Mira"
assert out["memory_patch"]["mood"] == "tender"
def test_call_turn_no_memory_on_short_caller(monkeypatch):
monkeypatch.setattr(proxy, "call_asr", lambda a: {"text": "hi"})
monkeypatch.setattr(proxy, "call_brain",
lambda s, m: {"text": "Hey there, friend.", "mood": "warm", "arc_cue": "none"})
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
out = proxy.call_turn("oblivious", 0, "<b64>")
assert out["queue_dedication"] is False and out["memory_patch"] is None
def test_segment_dedication_memory_aware(monkeypatch):
captured = {}
def _brain(system, messages):
captured["user"] = messages[-1]["content"]
return {"text": "This one's for the night-shift drivers, headlights home.",
"mood": "tender", "arc_cue": "none"}
monkeypatch.setattr(proxy, "call_brain", _brain)
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
out = proxy.segment_turn("dedication",
{"topic": "driving home after a double shift", "caller_name": "Mira", "mood": "tender"})
assert "Mira" in captured["user"] and "double shift" in captured["user"]
assert out["text"].startswith("This one's for")
def test_segment_dedication_no_memory_is_templated(monkeypatch):
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
out = proxy.segment_turn("dedication", None) # no model call on this path
assert out["text"].startswith("This next one's for") # from content.DEDICATIONS
def test_segment_dedication_junk_falls_back_to_name(monkeypatch):
monkeypatch.setattr(proxy, "call_brain", lambda s, m: {"text": "title: X", "mood": "warm", "arc_cue": "none"})
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
out = proxy.segment_turn("dedication", {"topic": "waiting up", "caller_name": "Sam"})
assert "Sam" in out["text"] # _dedication_fallback weaves the name
def test_song_intro_weaves_segue(monkeypatch):
captured = {}
def _brain(system, messages):
captured["user"] = messages[-1]["content"]
return {"text": "After that caller, a soft one -- 'Neon Rain'.", "mood": "tender", "arc_cue": "none"}
monkeypatch.setattr(proxy, "call_brain", _brain)
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
proxy.segment_turn("song_intro", {"title": "Neon Rain", "artist": "The Tuesday Ghosts",
"vibe": "melancholy", "segue": "right after that caller"})
assert "right after that caller" in captured["user"]
def test_song_intro_no_segue_when_absent(monkeypatch):
captured = {}
def _brain(system, messages):
captured["user"] = messages[-1]["content"]
return {"text": "Coming up, 'Neon Rain'.", "mood": "warm", "arc_cue": "none"}
monkeypatch.setattr(proxy, "call_brain", _brain)
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
proxy.segment_turn("song_intro", {"title": "Neon Rain", "artist": "The Tuesday Ghosts", "vibe": "melancholy"})
assert "through-line" not in captured["user"]
def test_fragment_user_mentions_another_station():
u = proxy._fragment_user(None)
assert "another station" in u and len(u) > 20
def test_segment_fragment_is_persona_free(monkeypatch):
captured = {}
def _brain(system, messages):
captured["system"] = system
return {"text": "calling all night nurses near mile marker nine", "mood": "warm", "arc_cue": "none"}
monkeypatch.setattr(proxy, "call_brain", _brain)
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
out = proxy.segment_turn("fragment", None)
assert arc.HOST_PERSONA not in captured["system"] # not the Sam Dusk host persona
assert out["text"].startswith("calling all night nurses")
def test_segment_fragment_junk_falls_back(monkeypatch):
monkeypatch.setattr(proxy, "call_brain", lambda s, m: {"text": "title: X", "mood": "warm", "arc_cue": "none"})
monkeypatch.setattr(proxy, "call_speak",
lambda t, voice=arc.VOICE: {"audio_b64": "", "words": [], "wtimes": [], "wdurations": []})
out = proxy.segment_turn("fragment", None)
assert out["text"] in content.FRAGMENTS # junk -> templated bank