| """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(): |
| |
| |
| 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(): |
| |
| 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(): |
| |
| 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(): |
| |
| |
| 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"] |
|
|
|
|
| |
| |
| |
| 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, |
| )) |
|
|
| |
| assert len(out) == 1 |
| assert out[0].startswith("__error__:") |
| assert "boom" in out[0] |
|
|
|
|
| |
| |
| |
|
|
| 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" |
|
|
|
|
| |
| |
| |
|
|
| 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 == [] |
|
|
|
|
| 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] |
| |
| 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."}] |
|
|
|
|
| |
| |
| |
|
|
| 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] |
| |
| assert "Mirror" in headings |
| |
| 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] |
| |
| 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 |
| 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 |
| 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 |
|
|