"""The FISHBOWL Gradio shell builds standalone (Unit 9) — mock-free. These tests exercise the *integrator* surface only: that the two-tab Blocks builds even when every leaf module (theme/render/session/show/lab) is absent (defensive placeholders), that the package import stays Gradio-free, that the root shim re-exports the app, and that the HTML-composition + fallback-session transport work end-to-end offline (no API key). """ from __future__ import annotations import gradio as gr def test_build_app_builds_with_placeholders() -> None: from src.ui.fishbowl.app import build_app demo = build_app() assert isinstance(demo, gr.Blocks) def test_module_level_demo_exists() -> None: from src.ui.fishbowl.app import demo assert isinstance(demo, gr.Blocks) def test_package_import_is_gradio_free() -> None: """Importing the package must not pull in Gradio (pure presenter promise).""" import subprocess import sys result = subprocess.run( [sys.executable, "-c", "import src.ui.fishbowl, sys; print('gradio' in sys.modules)"], capture_output=True, text=True, check=True, ) assert result.stdout.strip() == "False" def test_lazy_exports_resolve() -> None: import src.ui.fishbowl as pkg assert callable(pkg.build_app) assert isinstance(pkg.demo, gr.Blocks) def test_root_shim_imports() -> None: import app assert isinstance(app.demo, gr.Blocks) assert callable(app.launch) def test_fallback_session_transport_offline() -> None: """The placeholder session steps a real Conductor and snapshots a prefix view.""" from src.ui.fishbowl.app import SCENARIOS, _new_session name = next(iter(SCENARIOS.values())) session = _new_session(name) session.reset("") # default seed; genesis events appended assert session.head > 0 before = session.head session.step() assert session.head >= before # stepping never rewinds the head vm = session.snapshot(0) # pure prefix view at k=0 assert vm["step"] == 0 assert "cast" in vm and "feed" in vm vm_head = session.snapshot(session.head) assert vm_head["step"] == session.head def test_render_show_html_returns_four_panes() -> None: from src.ui.fishbowl.app import SCENARIOS, _new_session, render_show_html session = _new_session(next(iter(SCENARIOS.values()))) session.reset("") vm = session.snapshot(session.head) panes = render_show_html(vm, layout="constellation", mind_reader=True) assert isinstance(panes, tuple) and len(panes) == 4 assert all(isinstance(p, str) for p in panes) # Each layout produces a string stage without raising. for layout in ("constellation", "split", "feed"): stage, _feed, _meters, _verdict = render_show_html(vm, layout=layout, mind_reader=False) assert isinstance(stage, str) def test_render_show_html_overlays_thinking_strip_on_stage() -> None: # The "who's thinking…" hint rides on the always-visible stage pane (not the feed, # which resets its scroll on every re-render). It appears only when asked. from src.ui.fishbowl.app import SCENARIOS, _new_session, render_show_html session = _new_session(next(iter(SCENARIOS.values()))) session.reset("") vm = session.snapshot(session.head) stage_plain, *_ = render_show_html(vm, layout="constellation") assert "thinking-strip" not in stage_plain stage_hint, *_ = render_show_html(vm, layout="constellation", thinking="scene-whisperer is thinking") assert "thinking-strip" in stage_hint assert "scene-whisperer is thinking" in stage_hint # Prepended (first child) so position:sticky pins it to the TOP of the stage — in # view without scrolling, rather than below the fold at the bottom. assert stage_hint.index("thinking-strip") < stage_hint.index('class="constellation"') def test_thinking_strip_escapes_its_label() -> None: from src.ui.fishbowl.app import _thinking_strip html = _thinking_strip("") assert "