test_novapay_fixtures.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. """NovaPay 续约复审(**整改后**)数据房间的纯业务层查询测试。
  2. > 整改前("故意埋入 5 条风险线索")快照位于
  3. > ``data-room/NOVAPAY.original-2026-08-19/_fixtures-snapshot/``。
  4. > 当前 ``fixtures/`` 是整改后版本,对应 `policies/vendor-renewal.md` 的
  5. > ``renewal`` 档 —— 数据应当让 Agent 顺利得出可提交审批的结论。
  6. 覆盖以下场景(无需调用模型):
  7. 1. 供应商主数据:NovaPay 已从 ``renewal-review`` 回到 ``active``,
  8. 延迟交付率回落至历史基线,``notes`` 字段记录 PT-01 修复时间线。
  9. 2. 安全事故:2026-02-15 撞库事件状态为 ``closed``,且
  10. ``investigation_report`` 字段已由 ``null`` 变为 Atrium Labs 出具的
  11. 独立调查报告引用 —— 关键信息缺失被补齐。
  12. 3. 负面新闻:NovaPay 共 6 条记录,包含 ``recovered``、``denied``、
  13. ``confirmed`` 三种状态,覆盖 ``labor`` / ``financial`` / ``operational``
  14. 三个分类;声誉 Agent 应能识别"历史事件已结清"的语义。
  15. 4. 制裁名单:NovaPay 仍无命中,必须返回空数组而非抛错。
  16. 5. 跨供应商隔离:新增 NovaPay 记录不能影响 ACME 的查询结果数量。
  17. """
  18. from __future__ import annotations
  19. from pathlib import Path
  20. import unittest
  21. from vendor_guard.repository import JsonRepository
  22. PROJECT_ROOT = Path(__file__).resolve().parents[1]
  23. class NovapayFixtureTests(unittest.TestCase):
  24. """针对 ``fixtures/`` 中**整改后** NovaPay 记录的业务层查询行为。"""
  25. def setUp(self) -> None:
  26. self.repository = JsonRepository(PROJECT_ROOT / "fixtures")
  27. # -------- vendor_registry.json --------
  28. def test_vendor_registry_finds_novapay(self) -> None:
  29. vendor = self.repository.get_vendor("NOVAPAY")
  30. self.assertTrue(vendor["found"])
  31. self.assertEqual(vendor["vendor"], "NOVAPAY")
  32. self.assertEqual(vendor["status"], "active")
  33. self.assertTrue(vendor["beneficial_owner_verified"])
  34. def test_vendor_registry_lookup_is_case_insensitive_for_novapay(self) -> None:
  35. for variant in ("novapay", "NovaPay", "NOVAPAY", "NovaPAY"):
  36. with self.subTest(variant=variant):
  37. vendor = self.repository.get_vendor(variant)
  38. self.assertTrue(vendor["found"], msg=f"未命中:{variant}")
  39. self.assertEqual(vendor["vendor"], "NOVAPAY")
  40. def test_vendor_registry_shows_healthy_operating_signals(self) -> None:
  41. """整改后 ``late_delivery_rate`` 应当低于基线警戒值(0.10),
  42. ``disputes_24m`` 不应超过 2,且 ``notes`` 反映 PT-01 修复完成。"""
  43. vendor = self.repository.get_vendor("NOVAPAY")
  44. self.assertLess(vendor["late_delivery_rate"], 0.10)
  45. self.assertLessEqual(vendor["disputes_24m"], 2)
  46. self.assertIn("PT-01", vendor["notes"])
  47. self.assertIn("复测通过", vendor["notes"])
  48. def test_vendor_registry_addition_does_not_change_acme(self) -> None:
  49. acme = self.repository.get_vendor("ACME")
  50. self.assertTrue(acme["found"])
  51. self.assertEqual(acme["late_delivery_rate"], 0.08)
  52. self.assertEqual(acme["disputes_24m"], 2)
  53. self.assertNotIn("notes", acme)
  54. # -------- security_incidents.json --------
  55. def test_security_incident_returns_single_novapay_record(self) -> None:
  56. incidents = self.repository.get_incidents("NOVAPAY")
  57. self.assertEqual(len(incidents), 1)
  58. incident = incidents[0]
  59. self.assertEqual(incident["severity"], "high")
  60. self.assertEqual(incident["status"], "closed")
  61. self.assertEqual(incident["date"], "2026-02-15")
  62. def test_security_incident_now_carries_investigation_report(self) -> None:
  63. """整改前 ``investigation_report`` 为 ``null``(关键信息缺失)。
  64. 整改后必须引用 Atrium Labs 2026-04-10 独立调查报告,
  65. 让 Agent 能看到客观证据而不是空白。"""
  66. incidents = self.repository.get_incidents("NOVAPAY")
  67. self.assertEqual(len(incidents), 1)
  68. self.assertIn("investigation_report", incidents[0])
  69. report = incidents[0]["investigation_report"]
  70. self.assertIsNotNone(report)
  71. self.assertIn("Atrium Labs", report)
  72. self.assertIn("2026-04-10", report)
  73. def test_security_incident_lookup_is_case_insensitive_for_novapay(self) -> None:
  74. for variant in ("novapay", "NovaPay"):
  75. with self.subTest(variant=variant):
  76. incidents = self.repository.get_incidents(variant)
  77. self.assertEqual(len(incidents), 1)
  78. self.assertEqual(incidents[0]["vendor"].upper(), "NOVAPAY")
  79. def test_security_incident_isolation_acme_count_unchanged(self) -> None:
  80. incidents = self.repository.get_incidents("ACME")
  81. self.assertEqual(len(incidents), 1)
  82. self.assertEqual(incidents[0]["severity"], "medium")
  83. # -------- adverse_news.json --------
  84. def test_adverse_news_returns_all_six_novapay_records(self) -> None:
  85. news = self.repository.get_adverse_news("NOVAPAY")
  86. self.assertEqual(len(news), 6)
  87. dates = {item["date"] for item in news}
  88. self.assertEqual(
  89. dates,
  90. {
  91. "2026-04-22",
  92. "2026-06-10",
  93. "2026-07-03",
  94. "2026-07-19",
  95. "2026-08-05",
  96. "2026-08-12",
  97. },
  98. )
  99. def test_adverse_news_distinguishes_recovered_denied_confirmed(self) -> None:
  100. """整改后三类状态必须被透传,便于声誉 Agent 区分
  101. "历史事件已结清"(``recovered``)、"传言被否认"(``denied``)、
  102. "新近已验证事件"(``confirmed``)。"""
  103. news = self.repository.get_adverse_news("NOVAPAY")
  104. statuses = {item["status"] for item in news}
  105. self.assertEqual(statuses, {"recovered", "denied", "confirmed"})
  106. def test_adverse_news_covers_labor_financial_and_operational(self) -> None:
  107. news = self.repository.get_adverse_news("NOVAPAY")
  108. categories = {item["category"] for item in news}
  109. self.assertEqual(categories, {"labor", "financial", "operational"})
  110. def test_adverse_news_keeps_denied_record_distinct(self) -> None:
  111. """``denied`` 记录(TechRabbit 收购传闻 + NovaPay 官方否认)必须
  112. 与 ``confirmed`` / ``recovered`` 记录分开,便于声誉 Agent 按
  113. ``skills/reputation-review/SKILL.md`` 的来源优先级排序。"""
  114. news = self.repository.get_adverse_news("NOVAPAY")
  115. denied = [item for item in news if item["status"] == "denied"]
  116. confirmed = [item for item in news if item["status"] == "confirmed"]
  117. recovered = [item for item in news if item["status"] == "recovered"]
  118. self.assertEqual(len(denied), 1)
  119. self.assertEqual(len(confirmed), 2)
  120. self.assertEqual(len(recovered), 3)
  121. self.assertIn("收购", denied[0]["summary"])
  122. def test_adverse_news_recovered_records_carry_resolution_evidence(self) -> None:
  123. """``recovered`` 记录必须包含客观的恢复/结清证据,否则 Agent
  124. 无法判断历史事件的影响是否仍存在。允许使用 ``恢复`` /
  125. ``已完成`` / ``已结清`` 等多种表达。"""
  126. news = self.repository.get_adverse_news("NOVAPAY")
  127. recovered = [item for item in news if item["status"] == "recovered"]
  128. resolution_markers = ("恢复", "已完成", "已结清", "recovered")
  129. for item in recovered:
  130. with self.subTest(date=item["date"]):
  131. haystack = item["summary"] + item.get("source", "")
  132. self.assertTrue(
  133. any(marker in haystack for marker in resolution_markers),
  134. msg=f"recovered 记录缺少恢复证据:{item['date']}",
  135. )
  136. def test_adverse_news_lookup_is_case_insensitive_for_novapay(self) -> None:
  137. for variant in ("novapay", "NovaPay", "NOVAPAY"):
  138. with self.subTest(variant=variant):
  139. news = self.repository.get_adverse_news(variant)
  140. self.assertEqual(len(news), 6)
  141. def test_adverse_news_acme_count_unchanged(self) -> None:
  142. news = self.repository.get_adverse_news("ACME")
  143. self.assertEqual(len(news), 1)
  144. self.assertEqual(news[0]["status"], "settled")
  145. # -------- sanctions.json --------
  146. def test_sanctions_return_empty_for_novapay(self) -> None:
  147. result = self.repository.get_sanctions("NOVAPAY")
  148. self.assertEqual(result, [])
  149. def test_sanctions_lookup_is_case_insensitive_for_novapay(self) -> None:
  150. for variant in ("novapay", "NovaPay", "NOVAPAY"):
  151. with self.subTest(variant=variant):
  152. self.assertEqual(self.repository.get_sanctions(variant), [])
  153. def test_sanctions_still_empty_for_acme(self) -> None:
  154. self.assertEqual(self.repository.get_sanctions("ACME"), [])
  155. # -------- 跨供应商隔离(防止记录泄漏) --------
  156. def test_novapay_records_do_not_leak_into_acme_queries(self) -> None:
  157. self.assertEqual(len(self.repository.get_incidents("ACME")), 1)
  158. self.assertEqual(len(self.repository.get_adverse_news("ACME")), 1)
  159. # 反向:ACME 的事件也不应出现在 NovaPay 的查询中。
  160. novapay_incidents = self.repository.get_incidents("NOVAPAY")
  161. for incident in novapay_incidents:
  162. self.assertEqual(incident["vendor"], "NOVAPAY")
  163. novapay_news = self.repository.get_adverse_news("NOVAPAY")
  164. for item in novapay_news:
  165. self.assertEqual(item["vendor"], "NOVAPAY")
  166. if __name__ == "__main__":
  167. unittest.main()