test_candidate_decisions.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. from __future__ import annotations
  2. import pytest
  3. class CandidateRepository:
  4. def __init__(self):
  5. self.candidates = {
  6. "candidate-1": {
  7. "uid": "candidate-1",
  8. "normalized_data": {
  9. "code": "ORDER_ID",
  10. "name_zh": "订单编号",
  11. "name_en": "order_id",
  12. "definition": "订单的唯一业务标识",
  13. "logical_type": "string",
  14. },
  15. "evidence_uids": ["evidence-1"],
  16. "status": "candidate",
  17. },
  18. "candidate-2": {
  19. "uid": "candidate-2",
  20. "normalized_data": {"name_zh": "金额"},
  21. "evidence_uids": ["evidence-2"],
  22. "status": "candidate",
  23. },
  24. }
  25. self.decisions = []
  26. self.commits = 0
  27. self.rollbacks = 0
  28. def get(self, uid):
  29. return self.candidates.get(uid)
  30. def save_decisions(self, decisions):
  31. self.decisions.extend(decisions)
  32. def commit(self):
  33. self.commits += 1
  34. def rollback(self):
  35. self.rollbacks += 1
  36. class ElementService:
  37. def __init__(self):
  38. self.created = []
  39. def create_draft(self, payload, actor_uid):
  40. self.created.append((payload, actor_uid))
  41. return type("Element", (), {"uid": "element-new", "status": "draft"})()
  42. def test_batch_decisions_are_atomic_and_retain_evidence():
  43. from app.core.data_research.candidate_decisions import CandidateDecisionService
  44. repository = CandidateRepository()
  45. elements = ElementService()
  46. service = CandidateDecisionService(
  47. repository,
  48. data_elements=elements,
  49. uid_factory=iter(("decision-1", "decision-2")).__next__,
  50. )
  51. decisions = service.decide(
  52. [
  53. {"candidate_uid": "candidate-1", "action": "create"},
  54. {
  55. "candidate_uid": "candidate-2",
  56. "action": "map",
  57. "data_element_uid": "element-existing",
  58. },
  59. ],
  60. actor_uid="editor-1",
  61. )
  62. assert repository.commits == 1
  63. assert repository.rollbacks == 0
  64. assert decisions[0].data_element_uid == "element-new"
  65. assert decisions[0].evidence_uids == ("evidence-1",)
  66. assert elements.created[0][0]["evidence_uids"] == ["evidence-1"]
  67. assert elements.created[0][0].get("status") is None
  68. assert decisions[1].data_element_uid == "element-existing"
  69. def test_invalid_batch_rolls_back_without_partial_decisions():
  70. from app.core.data_research.candidate_decisions import CandidateDecisionService
  71. from app.core.data_research.errors import CandidateDecisionInvalid
  72. repository = CandidateRepository()
  73. service = CandidateDecisionService(
  74. repository,
  75. data_elements=ElementService(),
  76. )
  77. with pytest.raises(CandidateDecisionInvalid, match="data_element_uid"):
  78. service.decide(
  79. [
  80. {"candidate_uid": "candidate-1", "action": "ignore"},
  81. {"candidate_uid": "candidate-2", "action": "reuse"},
  82. ],
  83. actor_uid="editor-1",
  84. )
  85. assert repository.decisions == []
  86. assert repository.commits == 0
  87. assert repository.rollbacks == 1
  88. def test_candidate_decision_never_directly_publishes():
  89. from app.core.data_research.candidate_decisions import CandidateDecisionService
  90. from app.core.data_research.errors import CandidateDecisionInvalid
  91. repository = CandidateRepository()
  92. service = CandidateDecisionService(repository, data_elements=ElementService())
  93. with pytest.raises(CandidateDecisionInvalid, match="unsupported action"):
  94. service.decide(
  95. [{"candidate_uid": "candidate-1", "action": "publish"}],
  96. actor_uid="editor-1",
  97. )