Files
yovision/tests/test_bell_alert_contract.py
T

79 lines
3.5 KiB
Python
Raw Normal View History

"""Static contract checks for the T-020 Bell alert vertical slice."""
from __future__ import annotations
import json
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OPENAPI = ROOT / "docs" / "contracts" / "bell-alert-console-v1.openapi.json"
WEB = ROOT / "Bell" / "web" / "assets"
class BellAlertContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.contract = json.loads(OPENAPI.read_text(encoding="utf-8"))
cls.html = (WEB / "index.html").read_text(encoding="utf-8")
cls.css = (WEB / "style.css").read_text(encoding="utf-8")
cls.js = (WEB / "app.js").read_text(encoding="utf-8")
def test_loopback_routes_and_bounds_are_frozen(self) -> None:
self.assertEqual("3.1.0", self.contract["openapi"])
self.assertTrue(self.contract["servers"][0]["url"].startswith("http://127.0.0.1:"))
paths = self.contract["paths"]
self.assertEqual(
{"/alerts", "/alerts/{alert_id}", "/alerts/{alert_id}:ack", "/alerts/{alert_id}:close"},
set(paths),
)
limit = paths["/alerts"]["get"]["parameters"][1]["schema"]
self.assertEqual({"type": "integer", "minimum": 1, "maximum": 100, "default": 16}, limit)
key = self.contract["components"]["parameters"]["IdempotencyKey"]["schema"]
self.assertEqual((8, 128), (key["minLength"], key["maxLength"]))
def test_alert_is_separate_from_event_and_missing_capabilities_are_explicit(self) -> None:
detail = self.contract["components"]["schemas"]["AlertDetail"]
encoded = json.dumps(detail, ensure_ascii=False)
self.assertIn("events", encoded)
self.assertIn("transitions", encoded)
self.assertIn("evidence_status", encoded)
self.assertIn("delivery_status", encoded)
self.assertIn("证据切片尚未启用", self.html)
self.assertIn("升级与通知尚未启用", self.html)
self.assertNotRegex(self.html + self.js, r"短信已发送|语音已接通|倒计时[::]\s*\d")
def test_console_is_dependency_free_and_does_not_embed_secrets(self) -> None:
combined = self.html + self.css + self.js
self.assertNotRegex(combined, r"https?://|cdn\.|localStorage|sessionStorage")
self.assertNotRegex(combined, r"postgres(?:ql)?://|rtsp://|BELL_DB_DSN")
self.assertIn('type="password"', self.html)
self.assertIn('Cache-Control", "no-store"', (ROOT / "Bell" / "web" / "web.go").read_text(encoding="utf-8"))
def test_accessibility_and_small_screen_contract(self) -> None:
for marker in (
'aria-live="polite"',
'aria-live="assertive"',
'aria-busy="false"',
'aria-pressed="true"',
'href="#alerts"',
):
self.assertIn(marker, self.html)
self.assertIn("@media(max-width:480px)", self.css.replace(" ", ""))
self.assertIn("prefers-reduced-motion:reduce", self.css.replace(" ", ""))
self.assertIn("[hidden]{display:none!important}", self.css.replace(" ", ""))
self.assertRegex(self.css, r"min-height:44px")
self.assertIn("textContent", self.js)
self.assertNotIn("innerHTML", self.js)
def test_console_has_loading_empty_retry_conflict_and_keyboard_paths(self) -> None:
for marker in ("skeleton", "没有预警", "加载失败,重试", "already_acknowledged", "keydown"):
self.assertIn(marker, self.js)
self.assertRegex(self.js, re.escape('limit:"16"'))
if __name__ == "__main__":
unittest.main()