"""Tests for the SourceAdapter.preview_for_settings hook + framework rendering.""" from collections.abc import AsyncIterator from pathlib import Path import jinja2 import pytest from pydantic import BaseModel from central.adapter import SourceAdapter from central.models import Event TEMPLATES_DIR = Path(__file__).resolve().parents[1] / "src" / "central" / "gui" / "templates" class _StubSettings(BaseModel): pass class _StubAdapter(SourceAdapter): """Minimal SourceAdapter subclass that does NOT override preview_for_settings.""" name = "stub" display_name = "Stub Adapter" description = "Test fixture" settings_schema = _StubSettings default_cadence_s = 60 def __init__(self) -> None: pass async def poll(self) -> AsyncIterator[Event]: # pragma: no cover - never invoked if False: yield # type: ignore[unreachable] return async def apply_config(self, new_config) -> None: # pragma: no cover pass def subject_for(self, event: Event) -> str: # pragma: no cover return "stub" @pytest.mark.asyncio async def test_default_returns_none(): """The base SourceAdapter's preview_for_settings default returns None. Any adapter that does not override the hook gets a no-op preview, so the framework renders the page without a preview block (no crash, no opt-out boilerplate required in each adapter). """ adapter = _StubAdapter() result = await adapter.preview_for_settings(_StubSettings()) assert result is None def _render_partial(**context) -> str: env = jinja2.Environment( loader=jinja2.FileSystemLoader(str(TEMPLATES_DIR)), autoescape=jinja2.select_autoescape(["html"]), ) tmpl = env.get_template("_adapter_preview.html") return tmpl.render(**context) def test_partial_renders_list(): """Given list[dict] with insertion-ordered keys, partial renders a table. Framework is duck-typed: columns come from the first dict's keys. No adapter-name branches, no per-adapter Jinja. """ rows = [ {"a": "x1", "b": "y1"}, {"a": "x2", "b": "y2"}, ] out = _render_partial(preview_rows=rows, preview_error=None) # Header row uses first dict's keys in order. assert "