"""S5.2 — Varsling: unit/contract tests for ``notify.py`` (Notifier protocol + notifiers). Detach-seam (RED-when-detached) twins live in ``tests/test_notify_loadbearing.py``. Tests may import MAF-tainted modules freely (``verdicts``) — only ``notify.py`` itself must stay MAF-free. """ from __future__ import annotations import io from portfolio_optimiser.notify import ConsoleNotifier, Notifier from portfolio_optimiser.verdicts import ProposalFeatures, Verdict, capture_verdict def _verdict() -> Verdict: return capture_verdict( ProposalFeatures( affected_codes=frozenset({"05.2", "03.1"}), measure_type="scope_reduction", claimed_saving_nok=200_000.0, description="LED retrofit", ), "approved", "feasible within range", ) def test_plain_callable_satisfies_notifier() -> None: """The seam promise: any plain callable satisfies the runtime-checkable ``Notifier`` protocol, so ``run_project(notify=lambda v: ...)`` stays valid — a structural superset of ``Callable[[Verdict], None]``, not a new ABC (needs ``@runtime_checkable``).""" assert isinstance(lambda v: None, Notifier) def test_console_emits_id_and_decision() -> None: """ConsoleNotifier writes the verdict id + decision to the injected stream (never a URL).""" stream = io.StringIO() verdict = _verdict() ConsoleNotifier(stream=stream)(verdict) out = stream.getvalue() assert verdict.id in out assert verdict.decision in out