"""Tests for scripts/kb_proposal_normalize.py.""" from __future__ import annotations import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "scripts")) import kb_proposal_normalize as norm # noqa: E402 def test_rich_claim_split_stays_atomic_when_one_fragment_is_blocked(): preview = norm.normalize_proposal( { "id": "p-freeform", "proposal_type": "attach_evidence", "status": "approved", "payload": { "old_claim": {"id": "d0000000-0000-0000-0000-000000000005"}, "claim_candidates": [ { "claim_key": "new_claim_key", "headline": "New claim headline", "edges": [ { "edge_type": "supersedes_component_of", "target_claim_id": "d0000000-0000-0000-0000-000000000005", } ], "evidence": [{"source_key": "human_review"}], } ], "source_candidates": [{"source_key": "human_review"}], }, } ) assert preview["normalization_state"] == "blocked_rich_normalization" assert preview["strict_child_count"] == 0 assert preview["strict_child_proposals"] == [] assert preview["blocked_count"] == 1 assert "unsupported edge_type" in preview["blocked_fragments"][0]["reason"] assert "Resolve every blocked rich fragment" in preview["next_normalization_action"] def test_exact_edge_fragment_emits_strict_add_edge_child(): preview = norm.normalize_proposal( { "id": "p-edge", "proposal_type": "attach_evidence", "status": "approved", "payload": { "proposed_supersession_edges": [ { "from_claim": "11111111-1111-1111-1111-111111111111", "to_claim_id": "22222222-2222-2222-2222-222222222222", "edge_type": "supports", "weight": 0.7, } ] }, } ) assert preview["normalization_state"] == "strict_children_ready" assert preview["blocked_count"] == 0 assert preview["strict_child_count"] == 1 child = preview["strict_child_proposals"][0] assert child["proposal_type"] == "add_edge" assert child["payload"]["apply_payload"] == { "from_claim": "11111111-1111-1111-1111-111111111111", "to_claim": "22222222-2222-2222-2222-222222222222", "edge_type": "supports", "weight": 0.7, } def test_exact_evidence_fragments_emit_one_attach_evidence_child(): preview = norm.normalize_proposal( { "id": "p-evidence", "proposal_type": "attach_evidence", "status": "approved", "payload": { "evidence": [ { "claim_id": "33333333-3333-3333-3333-333333333333", "source_id": "44444444-4444-4444-4444-444444444444", "role": "grounds", "weight": 0.9, } ] }, } ) assert preview["normalization_state"] == "strict_children_ready" assert preview["strict_child_count"] == 1 child = preview["strict_child_proposals"][0] assert child["proposal_type"] == "attach_evidence" assert child["payload"]["apply_payload"]["evidence"] == [ { "claim_id": "33333333-3333-3333-3333-333333333333", "source_id": "44444444-4444-4444-4444-444444444444", "role": "grounds", "weight": 0.9, } ] def test_existing_apply_payload_is_already_strict(): preview = norm.normalize_proposal( { "id": "p-strict", "proposal_type": "add_edge", "status": "approved", "payload": { "apply_payload": { "from_claim": "11111111-1111-1111-1111-111111111111", "to_claim": "22222222-2222-2222-2222-222222222222", "edge_type": "supports", } }, } ) assert preview["normalization_state"] == "already_strict_apply_payload" assert preview["strict_child_count"] == 0 assert preview["blocked_count"] == 0 def test_rich_claim_graph_normalizes_to_one_atomic_approve_claim_bundle(): preview = norm.normalize_proposal( { "id": "77777777-7777-4777-8777-777777777777", "proposal_type": "attach_evidence", "status": "approved", "proposed_by_agent_id": "11111111-1111-4111-8111-111111111111", "source_ref": "telegram:test:approved", "payload": { "claim_candidates": [ { "claim_key": "canonical_test_claim", "headline": "A reviewed claim becomes canonical after approval.", "type": "structural", "confidence": 0.8, "tags": ["working-leo"], "evidence": [{"source_key": "canonical_test_source", "role": "grounds"}], } ], "proposed_claims": [ { "claim_key": "second_canonical_test_claim", "text": "A proposed_claims item is normalized too.", "type": "normative", } ], "source_candidates": [ { "source_key": "canonical_test_source", "source_type": "paper", "url": "https://example.test/canonical", "key_excerpt": "Reviewed source excerpt.", } ], }, } ) assert preview["normalization_state"] == "strict_children_ready" assert preview["blocked_count"] == 0 assert preview["strict_child_count"] == 1 child = preview["strict_child_proposals"][0] assert child["proposal_type"] == "approve_claim" bundle = child["payload"]["apply_payload"] assert bundle["agent_id"] == "11111111-1111-4111-8111-111111111111" assert len(bundle["claims"]) == 2 assert len(bundle["sources"]) == 3 # reviewed source plus two proposal-body sources assert len(bundle["evidence"]) == 3 assert bundle["edges"] == [] accounting = bundle["normalization_manifest"]["candidate_accounting"] assert accounting["input_collections"] == { "claim_candidates": 1, "proposed_claims": 1, "source_candidates": 1, } assert accounting["claim_candidates"]["input_count"] == 2 assert accounting["claim_candidates"]["planned_row_count"] == 2 assert accounting["source_candidates"]["input_count"] == 1 assert accounting["source_candidates"]["planned_row_count"] == 1 assert accounting["generated_claim_body_sources"]["planned_row_count"] == 2 assert accounting["total_planned_source_rows"] == 3 def test_rich_normalization_blocks_malformed_or_duplicate_candidates(): proposal = { "id": "77777777-7777-4777-8777-777777777777", "proposal_type": "attach_evidence", "status": "approved", "payload": { "claim_candidates": [ {"claim_key": "duplicate", "text": "first"}, {"claim_key": "duplicate", "text": "second"}, {"text": "missing key"}, "not-an-object", ], "proposed_claims": [ {"claim_key": "duplicate", "text": "duplicate across claim collections"}, {"claim_key": " ", "text": "blank key"}, 42, ], "source_candidates": [ {"source_key": "source", "source_type": "paper"}, {"source_key": "source", "source_type": "paper"}, ], }, } preview = norm.normalize_proposal(proposal) assert preview["normalization_state"] == "blocked_lossy_rich_normalization" assert preview["strict_child_count"] == 0 assert preview["strict_child_proposals"] == [] reasons = " ".join(item["reason"] for item in preview["blocked_fragments"]) assert "duplicate" in reasons assert "missing" in reasons assert "object" in reasons def test_rich_normalization_retains_type_mapping_manifest_for_review(): preview = norm.normalize_proposal( { "id": "77777777-7777-4777-8777-777777777778", "proposal_type": "attach_evidence", "status": "approved", "payload": { "claim_candidates": [ {"claim_key": "governance_claim", "text": "Governance process.", "type": "governance"}, {"claim_key": "typed_claim", "text": "Typed detail.", "type": "normative/detail"}, ], "source_candidates": [ {"source_key": "book_source", "source_type": "book", "key_excerpt": "Excerpt"}, {"source_key": "web_source", "url": "https://example.test/source"}, ], }, } ) child = preview["strict_child_proposals"][0] manifest = child["payload"]["apply_payload"]["normalization_manifest"] assert { (item["claim_key"], item["from"], item["to"]) for item in manifest["claim_type_mappings"] } == { ("governance_claim", "governance", "meta"), ("typed_claim", "normative/detail", "normative"), } assert { (item["source_key"], item["from"], item["to"]) for item in manifest["source_type_mappings"] } == { ("book_source", "book", "other"), ("web_source", "url", "article"), ("governance_claim__proposal_body", "proposal_body", "other"), ("typed_claim__proposal_body", "proposal_body", "other"), } def test_rich_normalization_rejects_non_list_candidate_collection(): preview = norm.normalize_proposal( { "id": "77777777-7777-4777-8777-777777777779", "proposal_type": "attach_evidence", "payload": {"claim_candidates": {"claim_key": "not-in-a-list"}}, } ) assert preview["normalization_state"] == "blocked_lossy_rich_normalization" assert preview["strict_child_proposals"] == [] assert preview["blocked_fragments"][0]["location"] == "payload.claim_candidates" assert "list of objects" in preview["blocked_fragments"][0]["reason"] def test_rich_normalization_fails_closed_when_planner_skips_candidate(monkeypatch): proposal = { "id": "77777777-7777-4777-8777-777777777780", "proposal_type": "attach_evidence", "payload": { "claim_candidates": [{"claim_key": "must_be_planned", "text": "Keep me."}], "source_candidates": [{"source_key": "must_be_planned_too", "source_type": "paper"}], }, } lossy_plan = norm.rich_plan.build_creation_plan(proposal) lossy_plan["planned_rows"]["claims"] = [] monkeypatch.setattr(norm.rich_plan, "build_creation_plan", lambda *_args, **_kwargs: lossy_plan) preview = norm.normalize_proposal(proposal) assert preview["normalization_state"] == "blocked_lossy_rich_normalization" assert preview["strict_child_proposals"] == [] assert preview["strict_child_count"] == 0 assert any("claim rows" in item["reason"] for item in preview["blocked_fragments"]) def test_source_candidates_without_claims_are_not_silently_dropped(): preview = norm.normalize_proposal( { "id": "77777777-7777-4777-8777-777777777781", "proposal_type": "attach_evidence", "payload": { "source_candidates": [ {"source_key": "orphan_source", "source_type": "paper", "key_excerpt": "Keep me."} ] }, } ) assert preview["normalization_state"] == "blocked_lossy_rich_normalization" assert preview["strict_child_proposals"] == [] assert "without at least one claim candidate" in preview["blocked_fragments"][0]["reason"]