# #region Test.DatasetReview.ClarificationHelpers [C:3] [TYPE Module] [SEMANTICS test,dataset,clarification,helpers,question,selection] # @BRIEF Tests for clarification_pkg/_helpers.py — pure helper functions: select_next_open_question, # count_resolved_questions, count_remaining_questions, normalize_answer_value, # build_impact_summary, upsert_clarification_finding, derive_readiness_state, derive_recommended_action. # @RELATION BINDS_TO -> [ClarificationHelpers] # @TEST_EDGE: select_next_open_question -> Priority ordering, no open questions # @TEST_EDGE: count_resolved_questions -> All answered, mixed, none # @TEST_EDGE: count_remaining_questions -> Open, skipped, expert review counts # @TEST_EDGE: normalize_answer_value -> Selected valid/invalid, skipped, expert, missing required # @TEST_EDGE: build_impact_summary -> SKIPPED, EXPERT_REVIEW, selected # @TEST_EDGE: upsert_clarification_finding -> New finding creation, existing update # @TEST_EDGE: derive_readiness_state -> Clarification active, needed, review ready # @TEST_EDGE: derive_recommended_action -> Answer next, start, review from __future__ import annotations from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest # ── Fixtures ──────────────────────────────────────────────── @pytest.fixture def mock_question(): from src.models.dataset_review import ClarificationQuestion, QuestionState q = MagicMock(spec=ClarificationQuestion) q.question_id = "q-1" q.topic_ref = "region_mapping" q.question_text = "What region does this dataset cover?" q.why_it_matters = "Region affects row-level access" q.current_guess = "US" q.priority = 5 q.state = QuestionState.OPEN q.created_at = datetime(2024, 6, 1, tzinfo=timezone.utc) q.answer = None q.clarification_session_id = "cs-1" return q @pytest.fixture def mock_option(): from src.models.dataset_review import ClarificationOption opt = MagicMock(spec=ClarificationOption) opt.option_id = "opt-1" opt.question_id = "q-1" opt.label = "US" opt.value = "US" opt.is_recommended = True opt.display_order = 1 return opt @pytest.fixture def mock_clarification_session(): from src.models.dataset_review import ClarificationSession, QuestionState cs = MagicMock(spec=ClarificationSession) cs.clarification_session_id = "cs-1" cs.current_question_id = "q-1" cs.status = MagicMock(value="active") cs.resolved_count = 0 cs.remaining_count = 3 cs.summary_delta = "" return cs # ── Tests for select_next_open_question ──────────────────── class TestSelectNextOpenQuestion: """select_next_open_question — priority-driven selection.""" def test_returns_highest_priority(self): from src.services.dataset_review.clarification_pkg._helpers import select_next_open_question from src.models.dataset_review import QuestionState q1 = MagicMock() q1.question_id = "q-1" q1.priority = 5 q1.state = QuestionState.OPEN q1.created_at = datetime(2024, 6, 1, tzinfo=timezone.utc) q2 = MagicMock() q2.question_id = "q-2" q2.priority = 10 q2.state = QuestionState.OPEN q2.created_at = datetime(2024, 6, 2, tzinfo=timezone.utc) cs = MagicMock() cs.questions = [q1, q2] result = select_next_open_question(cs) assert result is not None assert result.question_id == "q-2" # Higher priority first def test_returns_none_when_no_open(self): from src.services.dataset_review.clarification_pkg._helpers import select_next_open_question from src.models.dataset_review import QuestionState q1 = MagicMock() q1.state = QuestionState.ANSWERED cs = MagicMock() cs.questions = [q1] result = select_next_open_question(cs) assert result is None def test_empty_questions_returns_none(self): from src.services.dataset_review.clarification_pkg._helpers import select_next_open_question cs = MagicMock() cs.questions = [] result = select_next_open_question(cs) assert result is None # ── Tests for count_resolved_questions ───────────────────── class TestCountResolvedQuestions: """count_resolved_questions — count of ANSWERED questions.""" def test_all_answered(self): from src.services.dataset_review.clarification_pkg._helpers import count_resolved_questions from src.models.dataset_review import QuestionState cs = MagicMock() q1 = MagicMock() q1.state = QuestionState.ANSWERED q2 = MagicMock() q2.state = QuestionState.ANSWERED cs.questions = [q1, q2] assert count_resolved_questions(cs) == 2 def test_mixed_states(self): from src.services.dataset_review.clarification_pkg._helpers import count_resolved_questions from src.models.dataset_review import QuestionState cs = MagicMock() states = [QuestionState.ANSWERED, QuestionState.OPEN, QuestionState.SKIPPED] cs.questions = [ MagicMock(state=s) for s in states ] assert count_resolved_questions(cs) == 1 def test_no_questions(self): from src.services.dataset_review.clarification_pkg._helpers import count_resolved_questions cs = MagicMock() cs.questions = [] assert count_resolved_questions(cs) == 0 # ── Tests for count_remaining_questions ──────────────────── class TestCountRemainingQuestions: """count_remaining_questions — unresolved (open/skipped/expert) counts.""" def test_counts_open_skipped_expert(self): from src.services.dataset_review.clarification_pkg._helpers import count_remaining_questions from src.models.dataset_review import QuestionState cs = MagicMock() states = [QuestionState.OPEN, QuestionState.SKIPPED, QuestionState.EXPERT_REVIEW, QuestionState.ANSWERED] cs.questions = [ MagicMock(state=s) for s in states ] assert count_remaining_questions(cs) == 3 def test_all_resolved(self): from src.services.dataset_review.clarification_pkg._helpers import count_remaining_questions from src.models.dataset_review import QuestionState cs = MagicMock() cs.questions = [ MagicMock(state=QuestionState.ANSWERED), ] assert count_remaining_questions(cs) == 0 # ── Tests for normalize_answer_value ─────────────────────── class TestNormalizeAnswerValue: """normalize_answer_value — validation and normalization.""" def test_selected_valid(self): from src.services.dataset_review.clarification_pkg._helpers import normalize_answer_value from src.models.dataset_review import AnswerKind, ClarificationQuestion q = MagicMock(spec=ClarificationQuestion) opt = MagicMock() opt.value = "US" q.options = [opt] result = normalize_answer_value(AnswerKind.SELECTED, "US", q) assert result == "US" def test_selected_invalid_raises(self): from src.services.dataset_review.clarification_pkg._helpers import normalize_answer_value from src.models.dataset_review import AnswerKind, ClarificationQuestion q = MagicMock(spec=ClarificationQuestion) opt = MagicMock() opt.value = "US" q.options = [opt] with pytest.raises(ValueError, match="must match"): normalize_answer_value(AnswerKind.SELECTED, "EU", q) def test_selected_empty_value_raises(self): from src.services.dataset_review.clarification_pkg._helpers import normalize_answer_value from src.models.dataset_review import AnswerKind, ClarificationQuestion q = MagicMock(spec=ClarificationQuestion) q.options = [] with pytest.raises(ValueError, match="required"): normalize_answer_value(AnswerKind.SELECTED, "", q) def test_custom_requires_non_empty(self): from src.services.dataset_review.clarification_pkg._helpers import normalize_answer_value from src.models.dataset_review import AnswerKind, ClarificationQuestion q = MagicMock(spec=ClarificationQuestion) q.options = [] result = normalize_answer_value(AnswerKind.CUSTOM, "my_value", q) assert result == "my_value" def test_custom_empty_raises(self): from src.services.dataset_review.clarification_pkg._helpers import normalize_answer_value from src.models.dataset_review import AnswerKind, ClarificationQuestion q = MagicMock(spec=ClarificationQuestion) q.options = [] with pytest.raises(ValueError, match="required"): normalize_answer_value(AnswerKind.CUSTOM, "", q) def test_skipped_returns_skipped(self): from src.services.dataset_review.clarification_pkg._helpers import normalize_answer_value from src.models.dataset_review import AnswerKind, ClarificationQuestion q = MagicMock(spec=ClarificationQuestion) q.options = [] result = normalize_answer_value(AnswerKind.SKIPPED, None, q) assert result == "skipped" def test_expert_review_default(self): from src.services.dataset_review.clarification_pkg._helpers import normalize_answer_value from src.models.dataset_review import AnswerKind, ClarificationQuestion q = MagicMock(spec=ClarificationQuestion) q.options = [] result = normalize_answer_value(AnswerKind.EXPERT_REVIEW, None, q) assert result == "expert_review" # ── Tests for build_impact_summary ───────────────────────── class TestBuildImpactSummary: """build_impact_summary — readable audit note.""" def test_skipped(self): from src.services.dataset_review.clarification_pkg._helpers import build_impact_summary from src.models.dataset_review import AnswerKind q = MagicMock() q.topic_ref = "region_mapping" summary = build_impact_summary(q, AnswerKind.SKIPPED, None) assert "skipped" in summary assert "region_mapping" in summary def test_expert_review(self): from src.services.dataset_review.clarification_pkg._helpers import build_impact_summary from src.models.dataset_review import AnswerKind q = MagicMock() q.topic_ref = "region_mapping" summary = build_impact_summary(q, AnswerKind.EXPERT_REVIEW, None) assert "expert review" in summary def test_selected(self): from src.services.dataset_review.clarification_pkg._helpers import build_impact_summary from src.models.dataset_review import AnswerKind q = MagicMock() q.topic_ref = "region_mapping" summary = build_impact_summary(q, AnswerKind.SELECTED, "US") assert "US" in summary assert "recorded" in summary # ── Tests for upsert_clarification_finding ───────────────── class TestUpsertClarificationFinding: """upsert_clarification_finding — create or update findings per question.""" def test_creates_new_finding_for_selected(self): from src.services.dataset_review.clarification_pkg._helpers import upsert_clarification_finding from src.models.dataset_review import AnswerKind, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) session.session_id = "sess-1" session.findings = [] question = MagicMock() question.question_id = "q-1" question.topic_ref = "region_mapping" db = MagicMock() finding = upsert_clarification_finding(session, question, AnswerKind.SELECTED, "US", db) assert finding is not None assert finding.area.value == "clarification" assert finding.code == "CLARIFICATION_RESOLVED" assert finding.resolution_state.value == "resolved" db.add.assert_called_once() def test_creates_new_finding_for_skipped(self): from src.services.dataset_review.clarification_pkg._helpers import upsert_clarification_finding from src.models.dataset_review import AnswerKind, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) session.session_id = "sess-1" session.findings = [] question = MagicMock() question.question_id = "q-1" question.topic_ref = "region_mapping" db = MagicMock() finding = upsert_clarification_finding(session, question, AnswerKind.SKIPPED, None, db) assert finding.code == "CLARIFICATION_SKIPPED" assert finding.resolution_state.value == "skipped" def test_creates_new_finding_for_expert_review(self): from src.services.dataset_review.clarification_pkg._helpers import upsert_clarification_finding from src.models.dataset_review import AnswerKind, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) session.session_id = "sess-1" session.findings = [] question = MagicMock() question.question_id = "q-1" question.topic_ref = "region_mapping" db = MagicMock() finding = upsert_clarification_finding(session, question, AnswerKind.EXPERT_REVIEW, None, db) assert finding.code == "CLARIFICATION_EXPERT_REVIEW" assert finding.resolution_state.value == "expert_review" def test_updates_existing_finding(self): from src.services.dataset_review.clarification_pkg._helpers import upsert_clarification_finding from src.models.dataset_review import ( AnswerKind, DatasetReviewSession, ValidationFinding, FindingArea, ResolutionState, ) session = MagicMock(spec=DatasetReviewSession) session.session_id = "sess-1" existing = MagicMock(spec=ValidationFinding) existing.area = FindingArea.CLARIFICATION existing.caused_by_ref = "clarification:q-1" session.findings = [existing] question = MagicMock() question.question_id = "q-1" question.topic_ref = "region_mapping" db = MagicMock() finding = upsert_clarification_finding(session, question, AnswerKind.SELECTED, "US", db) # Should NOT call db.add for existing db.add.assert_not_called() assert finding.code == "CLARIFICATION_RESOLVED" def test_skipped_keeps_open_resolution_state(self): from src.services.dataset_review.clarification_pkg._helpers import upsert_clarification_finding from src.models.dataset_review import AnswerKind, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) session.session_id = "sess-1" session.findings = [] question = MagicMock() question.question_id = "q-1" question.topic_ref = "region_mapping" db = MagicMock() finding = upsert_clarification_finding(session, question, AnswerKind.SKIPPED, None, db) assert finding.resolved_at is None # ── Tests for derive_readiness_state ─────────────────────── class TestDeriveReadinessState: """derive_readiness_state — post-clarification readiness.""" def test_none_clarification_preserves_current(self): from src.services.dataset_review.clarification_pkg._helpers import derive_readiness_state from src.models.dataset_review import DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) session.readiness_state = MagicMock(value="review_ready") result = derive_readiness_state(session, None) assert result.value == "review_ready" def test_current_question_active(self): from src.services.dataset_review.clarification_pkg._helpers import derive_readiness_state from src.models.dataset_review import ClarificationSession, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) cs = MagicMock(spec=ClarificationSession) cs.current_question_id = "q-1" cs.remaining_count = 5 result = derive_readiness_state(session, cs) assert result.value == "clarification_active" def test_remaining_count_over_zero(self): from src.services.dataset_review.clarification_pkg._helpers import derive_readiness_state from src.models.dataset_review import ClarificationSession, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) cs = MagicMock(spec=ClarificationSession) cs.current_question_id = None cs.remaining_count = 2 result = derive_readiness_state(session, cs) assert result.value == "clarification_needed" def test_all_resolved(self): from src.services.dataset_review.clarification_pkg._helpers import derive_readiness_state from src.models.dataset_review import ClarificationSession, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) cs = MagicMock(spec=ClarificationSession) cs.current_question_id = None cs.remaining_count = 0 result = derive_readiness_state(session, cs) assert result.value == "review_ready" # ── Tests for derive_recommended_action ──────────────────── class TestDeriveRecommendedAction: """derive_recommended_action — post-clarification action guidance.""" def test_none_clarification_preserves_current(self): from src.services.dataset_review.clarification_pkg._helpers import derive_recommended_action from src.models.dataset_review import DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) session.recommended_action = MagicMock(value="launch_dataset") result = derive_recommended_action(session, None) assert result.value == "launch_dataset" def test_current_question_prompts_answer(self): from src.services.dataset_review.clarification_pkg._helpers import derive_recommended_action from src.models.dataset_review import ClarificationSession, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) cs = MagicMock(spec=ClarificationSession) cs.current_question_id = "q-1" cs.remaining_count = 3 result = derive_recommended_action(session, cs) assert result.value == "answer_next_question" def test_remaining_unresolved_prompts_start(self): from src.services.dataset_review.clarification_pkg._helpers import derive_recommended_action from src.models.dataset_review import ClarificationSession, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) cs = MagicMock(spec=ClarificationSession) cs.current_question_id = None cs.remaining_count = 2 result = derive_recommended_action(session, cs) assert result.value == "start_clarification" def test_all_resolved_returns_review(self): from src.services.dataset_review.clarification_pkg._helpers import derive_recommended_action from src.models.dataset_review import ClarificationSession, DatasetReviewSession session = MagicMock(spec=DatasetReviewSession) cs = MagicMock(spec=ClarificationSession) cs.current_question_id = None cs.remaining_count = 0 result = derive_recommended_action(session, cs) assert result.value == "review_documentation" # #endregion Test.DatasetReview.ClarificationHelpers