Kintsugi-Garden / tests /test_streaming.py
AI-Sherpa
Initial commit: Kintsugi Garden — Build Small Hackathon submission
4f04d05
Raw
History Blame Contribute Delete
11.8 kB
"""Unit tests for the streaming reflection path.
These tests cover the pure-Python pieces of the streaming refactor:
section flushing, sanitizer ordering, and safety short-circuit. They
do not touch FastAPI, gradio.Server, or any LLM backend — the
streaming dispatcher is mocked.
"""
import pytest
from app import _section_flush_loop
def _tokens(*chunks):
"""Helper: turn a list of strings into an iterator."""
for c in chunks:
yield c
def test_section_flush_emits_one_event_per_heading():
stream = _tokens(
"## Mirror\n", "One possible ", "reading is...\n",
"## Key Symbols\n", "- forest\n",
)
events = list(_section_flush_loop(stream))
headings = [e["heading"] for e in events]
assert headings == ["Mirror", "Key Symbols"]
def test_section_flush_runs_sanitizer_per_section():
# "You should" at sentence-start in Mirror; rewrite must apply
# when Mirror flushes (i.e. when ## Key Symbols arrives).
stream = _tokens(
"## Mirror\n", "You should ", "notice the forest.\n",
"## Key Symbols\n", "- forest\n",
)
events = list(_section_flush_loop(stream))
mirror = next(e for e in events if e["heading"] == "Mirror")
assert "you should" not in mirror["markdown"].lower()
assert "you might notice" in mirror["markdown"].lower()
def test_section_flush_emits_trailing_section():
# The final section has no following ## boundary — must still flush.
stream = _tokens(
"## Mirror\n", "...\n",
"## Gentle Question\n", "What might you notice?\n",
)
events = list(_section_flush_loop(stream))
assert events[-1]["heading"] == "Gentle Question"
assert "What might you notice?" in events[-1]["markdown"]
def test_section_flush_handles_split_heading():
# The `## ` boundary may straddle two tokens.
stream = _tokens("## Mirror\n...\n#", "# Key Symbols\n- forest\n")
events = list(_section_flush_loop(stream))
assert [e["heading"] for e in events] == ["Mirror", "Key Symbols"]
def test_section_flush_skips_preamble_before_first_heading():
# The model sometimes emits a stray newline before "## Mirror".
# Anything before the first ## is dropped.
stream = _tokens("\n\n## Mirror\nbody\n## Symbols\n- x\n")
events = list(_section_flush_loop(stream))
assert [e["heading"] for e in events] == ["Mirror", "Symbols"]
def test_section_flush_does_not_match_h3_subheading():
"""### Subheading inside a section body must not be parsed as a new section.
Regression: text.find("## ") matched the inner `## ` inside `###`.
"""
stream = _tokens(
"## Mirror\n",
"Some preamble.\n",
"### subsection\n",
"body inside subsection\n",
"## Symbols\n",
"- forest\n",
)
events = list(_section_flush_loop(stream))
headings = [e["heading"] for e in events]
assert headings == ["Mirror", "Symbols"]
mirror = next(e for e in events if e["heading"] == "Mirror")
assert "### subsection" in mirror["markdown"]
assert "body inside subsection" in mirror["markdown"]
# ---------------------------------------------------------------------------
# _run_llama_cpp_stream tests (Task 2)
# ---------------------------------------------------------------------------
from unittest.mock import MagicMock, patch
class _FakeLlama:
"""Stand-in for llama_cpp.Llama with create_chat_completion(stream=True)."""
def __init__(self, chunks):
self._chunks = chunks
self.last_kwargs = None
def create_chat_completion(self, **kwargs):
self.last_kwargs = kwargs
for piece in self._chunks:
yield {"choices": [{"delta": {"content": piece}}]}
def test_run_llama_cpp_stream_yields_token_chunks():
from app import _run_llama_cpp_stream
fake = _FakeLlama(["## Mirror\n", "hello", " world\n"])
with patch("app._load_llama_cpp_model", return_value=(fake, None)):
out = list(_run_llama_cpp_stream(
text="I dreamt of a forest",
entry_type="Dream",
depth=2,
symbol_matches=[],
grounded_jungian=False,
include_question=True,
))
assert out == ["## Mirror\n", "hello", " world\n"]
assert fake.last_kwargs["stream"] is True
def test_run_llama_cpp_stream_surfaces_load_error():
from app import _run_llama_cpp_stream
with patch("app._load_llama_cpp_model", return_value=(None, "boom")):
out = list(_run_llama_cpp_stream(
text="x", entry_type="Dream", depth=2,
symbol_matches=[], grounded_jungian=False, include_question=True,
))
# On load error, the streamer emits a single error sentinel and stops.
assert len(out) == 1
assert out[0].startswith("__error__:")
assert "boom" in out[0]
# ---------------------------------------------------------------------------
# run_model_stream dispatcher tests (Task 3)
# ---------------------------------------------------------------------------
def test_run_model_stream_dispatches_to_llama_cpp_by_default(monkeypatch):
import app as appmod
monkeypatch.setattr(appmod, "BACKEND", "llama_cpp")
called = []
def fake_stream(*args, **kwargs):
called.append(("llama_cpp", args, kwargs))
yield "hello"
monkeypatch.setattr(appmod, "_run_llama_cpp_stream", fake_stream)
out = list(appmod.run_model_stream(
text="x", entry_type="Dream", depth=2,
symbol_matches=[], grounded_jungian=False, include_question=True,
))
assert out == ["hello"]
assert called[0][0] == "llama_cpp"
def test_run_model_stream_dispatches_to_ollama_when_configured(monkeypatch):
import app as appmod
monkeypatch.setattr(appmod, "BACKEND", "ollama")
called = []
def fake_stream(*args, **kwargs):
called.append(("ollama",))
yield "world"
monkeypatch.setattr(appmod, "_run_ollama_stream", fake_stream)
out = list(appmod.run_model_stream(
text="x", entry_type="Dream", depth=2,
symbol_matches=[], grounded_jungian=False, include_question=True,
))
assert out == ["world"]
assert called[0][0] == "ollama"
# ---------------------------------------------------------------------------
# reflect_api generator tests (Task 4)
# ---------------------------------------------------------------------------
def test_reflect_api_safety_short_circuits_before_llm(monkeypatch):
import app as appmod
llm_called = []
def fake_stream(*a, **kw):
llm_called.append(True)
yield "## Mirror\nshould never appear\n"
monkeypatch.setattr(appmod, "run_model_stream", fake_stream)
events = list(appmod.reflect_api(
entry="I want to end it all tonight.",
entry_type="Journal",
depth=2,
grounded_jungian=False,
include_question=True,
))
types = [e["event"] for e in events]
assert "safety" in types
assert "reading_section" not in types
assert llm_called == [] # LLM must not have been called
def test_reflect_api_emits_expected_event_sequence(monkeypatch):
import app as appmod
def fake_stream(*a, **kw):
yield "## Mirror\nA possible reading.\n"
yield "## Key Symbols\n- forest\n"
monkeypatch.setattr(appmod, "run_model_stream", fake_stream)
monkeypatch.setattr(appmod, "render_mandala_to_path", lambda s: "fake.png")
events = list(appmod.reflect_api(
entry="I dreamt of a forest with a river.",
entry_type="Dream",
depth=2,
grounded_jungian=False,
include_question=True,
))
types = [e["event"] for e in events]
# Order: symbols first, then mandala, then per-section, then done.
assert types[0] == "symbols"
assert types[1] == "mandala"
assert types.count("reading_section") == 2
assert types[-1] == "done"
def test_reflect_api_surfaces_llm_error_sentinel(monkeypatch):
import app as appmod
def fake_stream(*a, **kw):
yield "__error__:llama.cpp call failed (RuntimeError: oom)."
monkeypatch.setattr(appmod, "run_model_stream", fake_stream)
monkeypatch.setattr(appmod, "render_mandala_to_path", lambda s: "fake.png")
events = list(appmod.reflect_api(
entry="x", entry_type="Journal", depth=2,
grounded_jungian=False, include_question=True,
))
types = [e["event"] for e in events]
assert "error" in types
err = next(e for e in events if e["event"] == "error")
assert "llama.cpp call failed" in err["message"]
def test_reflect_api_rejects_empty_entry():
import app as appmod
events = list(appmod.reflect_api(
entry=" ", entry_type="Journal", depth=2,
grounded_jungian=False, include_question=True,
))
assert events == [{"event": "error", "code": "empty",
"message": "Write something first."}]
# ---------------------------------------------------------------------------
# Task 4b: MAX_SECTION_BUFFER safety valve + reflect_api coverage gaps
# ---------------------------------------------------------------------------
def test_section_flush_max_buffer_emits_synthetic_heading():
"""If no ## boundary arrives, flush at MAX_SECTION_BUFFER as Stream."""
from app import _section_flush_loop, MAX_SECTION_BUFFER
big_blob = "x" * (MAX_SECTION_BUFFER + 100)
stream = _tokens("## Mirror\n", big_blob)
events = list(_section_flush_loop(stream))
headings = [e["heading"] for e in events]
# Mirror flushes when the synthetic boundary trips, with heading kept.
assert "Mirror" in headings
# And no event's body exceeds MAX_SECTION_BUFFER materially.
assert all(len(e["markdown"]) <= MAX_SECTION_BUFFER * 2 for e in events)
def test_reflect_api_continues_after_mandala_failure(monkeypatch):
"""A mandala render failure emits an error event but does NOT short-circuit.
The reading is the primary output; the mandala is decoration. Subsequent
reading_section events must still flow.
"""
import app as appmod
def fake_stream(*a, **kw):
yield "## Mirror\nA possible reading.\n"
def boom(symbol_matches):
raise RuntimeError("PIL exploded")
monkeypatch.setattr(appmod, "run_model_stream", fake_stream)
monkeypatch.setattr(appmod, "render_mandala_to_path", boom)
events = list(appmod.reflect_api(
entry="I dreamt of a forest with a river.",
entry_type="Dream",
depth=2,
grounded_jungian=False,
include_question=True,
))
types = [e["event"] for e in events]
# Symbols first, then error (from mandala), then reading_section, then done.
assert "symbols" in types
err = next(e for e in events if e["event"] == "error")
assert err["code"] == "mandala_failed"
assert "PIL exploded" in err["message"]
assert "reading_section" in types # Reading still flows.
assert types[-1] == "done"
def test_reflect_api_handles_empty_stream(monkeypatch):
"""If run_model_stream yields nothing, emit empty_stream error + done."""
import app as appmod
def fake_stream(*a, **kw):
if False:
yield # empty generator
return
monkeypatch.setattr(appmod, "run_model_stream", fake_stream)
monkeypatch.setattr(appmod, "render_mandala_to_path", lambda s: "fake.png")
events = list(appmod.reflect_api(
entry="I dreamt of a forest with a river.",
entry_type="Dream",
depth=2,
grounded_jungian=False,
include_question=True,
))
types = [e["event"] for e in events]
err = next(e for e in events if e["event"] == "error")
assert err["code"] == "empty_stream"
assert types[-1] == "done"
assert "reading_section" not in types