feat(s52): webhook notifier with fail-closed opt-in gate + injectable transport
This commit is contained in:
parent
f206dfbf65
commit
61c4cb2d26
3 changed files with 119 additions and 1 deletions
|
|
@ -14,10 +14,49 @@ from __future__ import annotations
|
|||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from portfolio_optimiser.notify import NotifyRefused, WebhookNotifier
|
||||
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict, capture_verdict
|
||||
|
||||
_SRC_DIR = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser"
|
||||
_MAF_ROOTS = {"agent_framework", "mcp"}
|
||||
|
||||
|
||||
def _verdict() -> Verdict:
|
||||
return capture_verdict(
|
||||
ProposalFeatures(
|
||||
affected_codes=frozenset({"05.2"}),
|
||||
measure_type="scope_reduction",
|
||||
claimed_saving_nok=200_000.0,
|
||||
),
|
||||
"approved",
|
||||
"expert reviewed (test)",
|
||||
)
|
||||
|
||||
|
||||
# --- LOAD-BEARING: egress gate (both branches, same url + same recording post) --------------------
|
||||
|
||||
|
||||
def test_webhook_gate_refuses_without_optin_and_posts_with_optin() -> None:
|
||||
"""LOAD-BEARING (no-silent-egress): without explicit per-run opt-in the webhook notifier
|
||||
refuses at CONSTRUCTION (``NotifyRefused``) and the transport is NEVER touched; with
|
||||
``allow_egress=True`` the same url + same transport delivers exactly one post. Detach the
|
||||
``if not allow_egress: raise`` gate → the False branch stops raising (silent egress) → RED."""
|
||||
url = "https://hooks.example.test/T000/B000/secret-token"
|
||||
posts: list[tuple[str, str]] = []
|
||||
|
||||
def recording_post(post_url: str, body: str) -> None:
|
||||
posts.append((post_url, body))
|
||||
|
||||
with pytest.raises(NotifyRefused):
|
||||
WebhookNotifier(url, allow_egress=False, post=recording_post)
|
||||
assert posts == [] # gate refused BEFORE any transport access
|
||||
|
||||
WebhookNotifier(url, allow_egress=True, post=recording_post)(_verdict())
|
||||
assert len(posts) == 1
|
||||
|
||||
|
||||
def test_notify_registered_maf_free() -> None:
|
||||
"""Meta: notify.py is registered in the MAF-free guard list, so ``test_okf_is_maf_free``
|
||||
actually scans it — otherwise the MAF-free claim would be green-but-dead (never checked)."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue