from __future__ import annotations from pathlib import Path from replacement_decision import ( VALID_REVIEW_STATES, VALID_SCOPES, build_replacement_audit, build_replacement_decision, matching_occurrence_ids, ) REPO_ROOT = Path(__file__).resolve().parents[1] UI_PLAN = REPO_ROOT / "REPLACE_LOGIC_UI_PLAN.md" THIS_TEST = Path(__file__) UI_ACTION_TO_STATE = { "Vervangen": "accepted", "Vervanging aanpassen": "edited", "Zichtbaar houden": "ignored", "Handmatig gemiste waarde toevoegen": "manual_added", "Als context behouden": "preserve_context", "Later controleren": "unresolved", } UI_SCOPE_TO_HELPER_SCOPE = { "Alleen deze plek": "this_occurrence", "Alle exact dezelfde waarden": "all_exact", "Alle genormaliseerde gelijke waarden": "all_normalized", } ALLOWED_VIEW_ONLY_SESSION_KEYS = { "replacement_decision_selected_occurrence_id", "replacement_decision_preview_state", "replacement_decision_preview_scope", "replacement_decision_preview_text", "replacement_decision_panel_expanded", } FORBIDDEN_MUTATION_TARGETS = { "replacement_editor", "edited_replacements_df", "review table rows", "export state", "scrub key state", "reinsert state", } def _ui_plan_text() -> str: return UI_PLAN.read_text(encoding="utf-8") def _contract_text() -> str: return _ui_plan_text().replace("`", "").lower() def test_ui_action_contract_maps_only_to_supported_helper_states(): assert set(UI_ACTION_TO_STATE.values()).issubset(VALID_REVIEW_STATES) for label, state in UI_ACTION_TO_STATE.items(): decision = build_replacement_decision( occurrence_id=f"occ-{state}", source_text="SYNTHETIC-WAARDE", entity_type="SYNTHETIC_ENTITY", display_label=label, suggested_replacement="[SYNTHETIC_1]", final_replacement="[SYNTHETIC_EDITED]" if state == "edited" else None, review_state=state, scope="this_occurrence", ) assert decision.review_state == state if state in {"ignored", "preserve_context", "unresolved"}: assert decision.creates_mapping is False assert decision.replacement_value is None else: assert decision.creates_mapping is True assert decision.replacement_value is not None def test_ui_scope_contract_maps_only_to_supported_helper_scopes_and_matching_rules(): assert set(UI_SCOPE_TO_HELPER_SCOPE.values()).issubset(VALID_SCOPES) occurrences = [ {"occurrence_id": "one", "source_text": "SYNTHETIC VALUE"}, {"occurrence_id": "two", "source_text": "SYNTHETIC VALUE"}, {"occurrence_id": "three", "source_text": "synthetic value"}, {"occurrence_id": "four", "source_text": "OTHER SYNTHETIC VALUE"}, ] exact_decision = build_replacement_decision( occurrence_id="one", source_text="SYNTHETIC VALUE", entity_type="SYNTHETIC_ENTITY", display_label="Vervangen", suggested_replacement="[SYNTHETIC_1]", review_state="accepted", scope="all_exact", ) normalized_decision = build_replacement_decision( occurrence_id="one", source_text="SYNTHETIC VALUE", entity_type="SYNTHETIC_ENTITY", display_label="Vervangen", suggested_replacement="[SYNTHETIC_1]", review_state="accepted", scope="all_normalized", ) assert matching_occurrence_ids(exact_decision, occurrences) == ["one", "two"] assert matching_occurrence_ids(normalized_decision, occurrences) == ["one", "two", "three"] def test_ui_plan_contains_required_labels_states_scopes_and_confirmation_language(): text = _ui_plan_text() lowered = text.lower() for label in UI_ACTION_TO_STATE: assert label in text for state in UI_ACTION_TO_STATE.values(): assert f"`{state}`" in text for label in UI_SCOPE_TO_HELPER_SCOPE: assert label in text for scope in UI_SCOPE_TO_HELPER_SCOPE.values(): assert f"`{scope}`" in text assert "affected count" in lowered assert "stronger confirmation" in lowered assert "no fuzzy matching or guessed intent" in lowered assert "default scope is `this_occurrence`" in lowered def test_audit_contract_is_report_only_and_export_readiness_is_advisory(): decisions = [ build_replacement_decision( occurrence_id="accepted-1", source_text="SYNTHETIC-A", entity_type="SYNTHETIC_ENTITY", display_label="Vervangen", suggested_replacement="[SYNTHETIC_A]", review_state="accepted", ), build_replacement_decision( occurrence_id="later-1", source_text="SYNTHETIC-B", entity_type="SYNTHETIC_ENTITY", display_label="Later controleren", suggested_replacement="[SYNTHETIC_B]", review_state="unresolved", risk_flags=("synthetic_high_risk",), ), ] audit = build_replacement_audit(decisions) assert audit["report_only"] is True assert audit["export_blocking"] is False assert audit["export_readiness"] == "high_risk_unresolved" assert audit["unresolved_items"] == ["later-1"] assert audit["mapping_candidates"] == ["accepted-1"] assert audit["risk_flags"] == ["synthetic_high_risk"] def test_ui_contract_plan_preserves_boundaries_and_does_not_approve_ui_implementation(): text = _contract_text() for required in [ "does not implement ui", "audit panel is report-only", "must not block export", "does not approve export blocking", "must not change scrub key behavior", "highlight preview remains read-only and non-authoritative", "no click-to-mark implementation is approved", "this plan does not change", "presidio_streamlit.py", "serial_review_panel_ui.py", "fix_streamlit_nested_expanders.py", "review table behavior", "export/download behavior", "scrub key behavior", "reinsert behavior", "helper runtime behavior", "dependencies", "cloud processing", "real-data fixtures", ]: assert required in text def test_staged_vs_applied_state_contract_is_explicit_and_non_mutating(): text = _contract_text() for required in [ "staged decision preview only", "staged decision state is not applied state", "existing review table remains source of truth and fallback", "no review table mutation", "no replacement mutation", "no automatic replacement", "not write those decisions back", "separate package with explicit coordinator approval", ]: assert required in text def test_session_state_contract_allows_only_view_only_keys_and_blocks_mutation_targets(): text = _ui_plan_text() contract = _contract_text() assert "Allowed view-only session keys" in text assert "temporary UI selection and preview state" in text assert "must not be treated as applied replacement state" in text for key in ALLOWED_VIEW_ONLY_SESSION_KEYS: assert key in text for target in FORBIDDEN_MUTATION_TARGETS: assert f"no mutation of {target}" in contract def test_scrub_key_mapping_indicators_are_advisory_only(): text = _contract_text() decisions = [ build_replacement_decision( occurrence_id="mapping-1", source_text="SYNTHETIC-A", entity_type="SYNTHETIC_ENTITY", display_label="Vervangen", suggested_replacement="[SYNTHETIC_A]", review_state="accepted", ) ] audit = build_replacement_audit(decisions) assert decisions[0].creates_mapping is True assert audit["mapping_candidates"] == ["mapping-1"] assert "creates_mapping is advisory only" in text assert "does not authorize a scrub key write" in text assert "mapping_candidates are advisory only" in text assert "do not authorize scrub key persistence" in text assert "writing mappings directly from a new ui panel" in text assert "scrub key schema" in text def test_export_download_and_reinsert_boundaries_are_explicit(): text = _contract_text() for required in [ "export_readiness is advisory only", "must not call export/download functions", "change export eligibility", "disable export buttons", "alter download payloads", "no export/download calls", "no reinsert behavior change", "no scrub key writes", ]: assert required in text def test_no_fuzzy_matching_no_guessed_intent_and_no_automatic_replacement_contract(): text = _contract_text() assert "no fuzzy matching or guessed intent is allowed" in text assert "no automatic replacement" in text assert "auto-repairing duplicate placeholders" in text def test_all_normalized_not_available_as_first_mutating_scope_without_approval(): text = _contract_text() assert "all_normalized" in text assert "not available as a first mutating ui scope without separate explicit coordinator approval" in text assert "disabled/advisory only" in text def test_future_ui_requires_separate_explicit_coordinator_approval(): text = _contract_text() assert "only after separate explicit coordinator approval" in text assert "do not start implementation automatically" in text assert "small staged/read-only companion panel" in text def test_contract_tests_use_synthetic_values_only(): rendered = THIS_TEST.read_text(encoding="utf-8") + _ui_plan_text() forbidden_real_data_examples = [ "Jan " + "Jansen", "Piet " + "de " + "Vries", "123" + "456" + "782", ] assert "SYNTHETIC" in rendered for forbidden in forbidden_real_data_examples: assert forbidden not in rendered