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")]