"""NovaPay 续约复审(**整改后**)数据房间的纯业务层查询测试。 > 整改前("故意埋入 5 条风险线索")快照位于 > ``data-room/NOVAPAY.original-2026-08-19/_fixtures-snapshot/``。 > 当前 ``fixtures/`` 是整改后版本,对应 `policies/vendor-renewal.md` 的 > ``renewal`` 档 —— 数据应当让 Agent 顺利得出可提交审批的结论。 覆盖以下场景(无需调用模型): 1. 供应商主数据:NovaPay 已从 ``renewal-review`` 回到 ``active``, 延迟交付率回落至历史基线,``notes`` 字段记录 PT-01 修复时间线。 2. 安全事故:2026-02-15 撞库事件状态为 ``closed``,且 ``investigation_report`` 字段已由 ``null`` 变为 Atrium Labs 出具的 独立调查报告引用 —— 关键信息缺失被补齐。 3. 负面新闻:NovaPay 共 6 条记录,包含 ``recovered``、``denied``、 ``confirmed`` 三种状态,覆盖 ``labor`` / ``financial`` / ``operational`` 三个分类;声誉 Agent 应能识别"历史事件已结清"的语义。 4. 制裁名单:NovaPay 仍无命中,必须返回空数组而非抛错。 5. 跨供应商隔离:新增 NovaPay 记录不能影响 ACME 的查询结果数量。 """ from __future__ import annotations from pathlib import Path import unittest from vendor_guard.repository import JsonRepository PROJECT_ROOT = Path(__file__).resolve().parents[1] class NovapayFixtureTests(unittest.TestCase): """针对 ``fixtures/`` 中**整改后** NovaPay 记录的业务层查询行为。""" def setUp(self) -> None: self.repository = JsonRepository(PROJECT_ROOT / "fixtures") # -------- vendor_registry.json -------- def test_vendor_registry_finds_novapay(self) -> None: vendor = self.repository.get_vendor("NOVAPAY") self.assertTrue(vendor["found"]) self.assertEqual(vendor["vendor"], "NOVAPAY") self.assertEqual(vendor["status"], "active") self.assertTrue(vendor["beneficial_owner_verified"]) def test_vendor_registry_lookup_is_case_insensitive_for_novapay(self) -> None: for variant in ("novapay", "NovaPay", "NOVAPAY", "NovaPAY"): with self.subTest(variant=variant): vendor = self.repository.get_vendor(variant) self.assertTrue(vendor["found"], msg=f"未命中:{variant}") self.assertEqual(vendor["vendor"], "NOVAPAY") def test_vendor_registry_shows_healthy_operating_signals(self) -> None: """整改后 ``late_delivery_rate`` 应当低于基线警戒值(0.10), ``disputes_24m`` 不应超过 2,且 ``notes`` 反映 PT-01 修复完成。""" vendor = self.repository.get_vendor("NOVAPAY") self.assertLess(vendor["late_delivery_rate"], 0.10) self.assertLessEqual(vendor["disputes_24m"], 2) self.assertIn("PT-01", vendor["notes"]) self.assertIn("复测通过", vendor["notes"]) def test_vendor_registry_addition_does_not_change_acme(self) -> None: acme = self.repository.get_vendor("ACME") self.assertTrue(acme["found"]) self.assertEqual(acme["late_delivery_rate"], 0.08) self.assertEqual(acme["disputes_24m"], 2) self.assertNotIn("notes", acme) # -------- security_incidents.json -------- def test_security_incident_returns_single_novapay_record(self) -> None: incidents = self.repository.get_incidents("NOVAPAY") self.assertEqual(len(incidents), 1) incident = incidents[0] self.assertEqual(incident["severity"], "high") self.assertEqual(incident["status"], "closed") self.assertEqual(incident["date"], "2026-02-15") def test_security_incident_now_carries_investigation_report(self) -> None: """整改前 ``investigation_report`` 为 ``null``(关键信息缺失)。 整改后必须引用 Atrium Labs 2026-04-10 独立调查报告, 让 Agent 能看到客观证据而不是空白。""" incidents = self.repository.get_incidents("NOVAPAY") self.assertEqual(len(incidents), 1) self.assertIn("investigation_report", incidents[0]) report = incidents[0]["investigation_report"] self.assertIsNotNone(report) self.assertIn("Atrium Labs", report) self.assertIn("2026-04-10", report) def test_security_incident_lookup_is_case_insensitive_for_novapay(self) -> None: for variant in ("novapay", "NovaPay"): with self.subTest(variant=variant): incidents = self.repository.get_incidents(variant) self.assertEqual(len(incidents), 1) self.assertEqual(incidents[0]["vendor"].upper(), "NOVAPAY") def test_security_incident_isolation_acme_count_unchanged(self) -> None: incidents = self.repository.get_incidents("ACME") self.assertEqual(len(incidents), 1) self.assertEqual(incidents[0]["severity"], "medium") # -------- adverse_news.json -------- def test_adverse_news_returns_all_six_novapay_records(self) -> None: news = self.repository.get_adverse_news("NOVAPAY") self.assertEqual(len(news), 6) dates = {item["date"] for item in news} self.assertEqual( dates, { "2026-04-22", "2026-06-10", "2026-07-03", "2026-07-19", "2026-08-05", "2026-08-12", }, ) def test_adverse_news_distinguishes_recovered_denied_confirmed(self) -> None: """整改后三类状态必须被透传,便于声誉 Agent 区分 "历史事件已结清"(``recovered``)、"传言被否认"(``denied``)、 "新近已验证事件"(``confirmed``)。""" news = self.repository.get_adverse_news("NOVAPAY") statuses = {item["status"] for item in news} self.assertEqual(statuses, {"recovered", "denied", "confirmed"}) def test_adverse_news_covers_labor_financial_and_operational(self) -> None: news = self.repository.get_adverse_news("NOVAPAY") categories = {item["category"] for item in news} self.assertEqual(categories, {"labor", "financial", "operational"}) def test_adverse_news_keeps_denied_record_distinct(self) -> None: """``denied`` 记录(TechRabbit 收购传闻 + NovaPay 官方否认)必须 与 ``confirmed`` / ``recovered`` 记录分开,便于声誉 Agent 按 ``skills/reputation-review/SKILL.md`` 的来源优先级排序。""" news = self.repository.get_adverse_news("NOVAPAY") denied = [item for item in news if item["status"] == "denied"] confirmed = [item for item in news if item["status"] == "confirmed"] recovered = [item for item in news if item["status"] == "recovered"] self.assertEqual(len(denied), 1) self.assertEqual(len(confirmed), 2) self.assertEqual(len(recovered), 3) self.assertIn("收购", denied[0]["summary"]) def test_adverse_news_recovered_records_carry_resolution_evidence(self) -> None: """``recovered`` 记录必须包含客观的恢复/结清证据,否则 Agent 无法判断历史事件的影响是否仍存在。允许使用 ``恢复`` / ``已完成`` / ``已结清`` 等多种表达。""" news = self.repository.get_adverse_news("NOVAPAY") recovered = [item for item in news if item["status"] == "recovered"] resolution_markers = ("恢复", "已完成", "已结清", "recovered") for item in recovered: with self.subTest(date=item["date"]): haystack = item["summary"] + item.get("source", "") self.assertTrue( any(marker in haystack for marker in resolution_markers), msg=f"recovered 记录缺少恢复证据:{item['date']}", ) def test_adverse_news_lookup_is_case_insensitive_for_novapay(self) -> None: for variant in ("novapay", "NovaPay", "NOVAPAY"): with self.subTest(variant=variant): news = self.repository.get_adverse_news(variant) self.assertEqual(len(news), 6) def test_adverse_news_acme_count_unchanged(self) -> None: news = self.repository.get_adverse_news("ACME") self.assertEqual(len(news), 1) self.assertEqual(news[0]["status"], "settled") # -------- sanctions.json -------- def test_sanctions_return_empty_for_novapay(self) -> None: result = self.repository.get_sanctions("NOVAPAY") self.assertEqual(result, []) def test_sanctions_lookup_is_case_insensitive_for_novapay(self) -> None: for variant in ("novapay", "NovaPay", "NOVAPAY"): with self.subTest(variant=variant): self.assertEqual(self.repository.get_sanctions(variant), []) def test_sanctions_still_empty_for_acme(self) -> None: self.assertEqual(self.repository.get_sanctions("ACME"), []) # -------- 跨供应商隔离(防止记录泄漏) -------- def test_novapay_records_do_not_leak_into_acme_queries(self) -> None: self.assertEqual(len(self.repository.get_incidents("ACME")), 1) self.assertEqual(len(self.repository.get_adverse_news("ACME")), 1) # 反向:ACME 的事件也不应出现在 NovaPay 的查询中。 novapay_incidents = self.repository.get_incidents("NOVAPAY") for incident in novapay_incidents: self.assertEqual(incident["vendor"], "NOVAPAY") novapay_news = self.repository.get_adverse_news("NOVAPAY") for item in novapay_news: self.assertEqual(item["vendor"], "NOVAPAY") if __name__ == "__main__": unittest.main()