from app.schemas.graph import HTML5VisualPayload
from app.services.visualization_service import VisualizationService
class _CalledOrRaise:
"""Fake engine base: records calls, or raises if it must never be called."""
def __init__(self, marker: str, should_raise: bool = False):
self.marker = marker
self.should_raise = should_raise
self.calls = []
def _payload(self, animation_type="2d_text"):
return HTML5VisualPayload(
html_code="",
animation_type=animation_type,
explanation=self.marker,
)
class FakeRouter:
def __init__(self, decision=None):
self.decision = decision
self.calls = []
def classify(self, selection_text, card_markdown, chunks, familiarity):
self.calls.append((selection_text, card_markdown, chunks, familiarity))
return self.decision
class FakeFormula(_CalledOrRaise):
def __init__(self):
super().__init__("FAKE_FORMULA")
def generate(self, concept, chunks, familiarity):
self.calls.append((concept, chunks, familiarity))
if self.should_raise:
raise AssertionError("formula engine should not have been called")
return self._payload("formula")
class FakeTextRef(_CalledOrRaise):
def __init__(self):
super().__init__("FAKE_TEXT_REF")
def generate(self, concept, chunks, familiarity, web_results=None):
self.calls.append((concept, chunks, familiarity, web_results))
if self.should_raise:
raise AssertionError("text_ref engine should not have been called")
return self._payload("2d_text")
class FakeD3(_CalledOrRaise):
def __init__(self):
super().__init__("FAKE_D3")
def generate(self, concept, chunks, familiarity):
self.calls.append((concept, chunks, familiarity))
if self.should_raise:
raise AssertionError("d3 engine should not have been called")
return self._payload("graph")
class FakeShell(_CalledOrRaise):
def __init__(self):
super().__init__("FAKE_SHELL")
def generate(self, concept, kind, chunks, familiarity):
self.calls.append((concept, kind, chunks, familiarity))
if self.should_raise:
raise AssertionError("shell engine should not have been called")
return self._payload(kind)
def _make_service(router=None):
return VisualizationService(
router=router or FakeRouter(),
formula=FakeFormula(),
text_ref=FakeTextRef(),
d3=FakeD3(),
shell=FakeShell(),
)
_CHUNKS = [{"source": "s", "text": "some grounded text"}]
def test_generate_formula_routes_to_formula_engine():
service = _make_service()
out = service.generate("E=mc^2", "formula", "graduate", chunks=_CHUNKS)
assert out.explanation == "FAKE_FORMULA"
assert service.formula.calls == [("E=mc^2", _CHUNKS, "graduate")]
assert service.text_ref.calls == []
assert service.d3.calls == []
assert service.shell.calls == []
def test_generate_2d_text_routes_to_text_ref_engine_with_web_results():
service = _make_service()
web_results = [{"title": "t", "url": "u"}]
out = service.generate(
"some concept", "2d_text", "graduate", chunks=_CHUNKS, web_results=web_results
)
assert out.explanation == "FAKE_TEXT_REF"
assert service.text_ref.calls == [("some concept", _CHUNKS, "graduate", web_results)]
assert service.formula.calls == []
assert service.d3.calls == []
assert service.shell.calls == []
def test_generate_graph_routes_to_d3_engine():
service = _make_service()
out = service.generate("GDP over time", "graph", "graduate", chunks=_CHUNKS)
assert out.explanation == "FAKE_D3"
assert service.d3.calls == [("GDP over time", _CHUNKS, "graduate")]
assert service.formula.calls == []
assert service.text_ref.calls == []
assert service.shell.calls == []
def test_generate_3d_routes_to_shell_engine_with_kind_3d():
service = _make_service()
out = service.generate("a molecule", "3d", "graduate", chunks=_CHUNKS)
assert out.explanation == "FAKE_SHELL"
assert service.shell.calls == [("a molecule", "3d", _CHUNKS, "graduate")]
assert service.formula.calls == []
assert service.text_ref.calls == []
assert service.d3.calls == []
def test_generate_2d_anim_routes_to_shell_engine_with_kind_2d_anim():
service = _make_service()
out = service.generate("a mechanism", "2d_anim", "graduate", chunks=_CHUNKS)
assert out.explanation == "FAKE_SHELL"
assert service.shell.calls == [("a mechanism", "2d_anim", _CHUNKS, "graduate")]
assert service.formula.calls == []
assert service.text_ref.calls == []
assert service.d3.calls == []
def test_generate_decline_returns_decline_card_without_calling_any_engine():
service = VisualizationService(
router=FakeRouter(),
formula=FakeFormula(),
text_ref=FakeTextRef(),
d3=FakeD3(),
shell=FakeShell(),
)
for engine in (service.formula, service.text_ref, service.d3, service.shell):
engine.should_raise = True
out = service.generate("pure philosophy", "decline", "graduate", chunks=_CHUNKS)
assert out.animation_type == "2d_text"
assert "better discussed in chat" in out.html_code
assert service.formula.calls == []
assert service.text_ref.calls == []
assert service.d3.calls == []
assert service.shell.calls == []
def test_generate_no_chunks_declines_regardless_of_modality():
for engine in (FakeFormula(), FakeTextRef(), FakeD3(), FakeShell()):
engine.should_raise = True
for modality in ("formula", "2d_text", "graph", "3d", "2d_anim", "decline"):
service = VisualizationService(
router=FakeRouter(),
formula=FakeFormula(),
text_ref=FakeTextRef(),
d3=FakeD3(),
shell=FakeShell(),
)
for engine in (service.formula, service.text_ref, service.d3, service.shell):
engine.should_raise = True
out = service.generate("no source concept", modality, "graduate", chunks=[])
assert out.animation_type == "2d_text"
assert "source grounding" in out.explanation or "better discussed in chat" in out.html_code
assert service.formula.calls == []
assert service.text_ref.calls == []
assert service.d3.calls == []
assert service.shell.calls == []
# Also confirm chunks=None short-circuits the same way.
service = _make_service()
for engine in (service.formula, service.text_ref, service.d3, service.shell):
engine.should_raise = True
out = service.generate("no source concept", "formula", "graduate", chunks=None)
assert out.animation_type == "2d_text"
assert service.formula.calls == []
def test_classify_delegates_to_router():
from app.agents.modality_router import ModalityDecision
decision = ModalityDecision(modality="formula", reasoning="has an equation")
router = FakeRouter(decision=decision)
service = _make_service(router=router)
out = service.classify("selected text", "# card markdown", _CHUNKS, "graduate")
assert out is decision
assert router.calls == [("selected text", "# card markdown", _CHUNKS, "graduate")]