"""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()