"""Tests for the party-claims executive summary (#202, WS3). Cover the deterministic, offline parts: - ``_build_arguments_context`` source selection: legal_arguments preferred, raw claims fallback, "none" when empty (G2 — single source, no re-extract). - ``summarize_party_claims`` pins Opus 4.8 + effort=high and grounds the prompt strictly in the case's own claims (anti-hallucination / INV-AH). - ``_render_markdown_body`` maps markdown headings/bullets onto template styles. Generation itself (claude -p) is local-only and not exercised here — the claude_session call is monkeypatched. """ from __future__ import annotations import asyncio from uuid import uuid4 from legal_mcp.services import party_claims_summary as pcs def _run(coro): return asyncio.run(coro) # ── source selection (G2: arguments preferred, claims fallback) ──── def test_arguments_preferred_over_claims(monkeypatch): cid = uuid4() async def _get_args(_case_id): return [ {"party": "appellant", "argument_title": "חריגת בנייה", "argument_body": "הבנייה חורגת מקו הבניין", "legal_topic": "קווי בניין"}, {"party": "respondent", "argument_title": "התיישנות", "argument_body": "", "legal_topic": ""}, ] async def _get_claims(_case_id, *a, **k): # must NOT be consulted raise AssertionError("claims fallback used despite arguments existing") monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _get_args) monkeypatch.setattr(pcs.db, "get_claims", _get_claims) ctx, n, kind = _run(pcs._build_arguments_context(cid)) assert kind == "arguments" assert n == 2 assert "העוררים" in ctx and "המשיבים" in ctx assert "חריגת בנייה" in ctx def test_falls_back_to_claims_when_no_arguments(monkeypatch): cid = uuid4() async def _no_args(_case_id): return [] async def _claims(_case_id, *a, **k): return [ {"party_role": "appellant", "claim_text": "טענה אחת", "source_document": "appeal"}, {"party_role": "respondent", "claim_text": "טענה שתיים", "source_document": "response"}, ] monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _no_args) monkeypatch.setattr(pcs.db, "get_claims", _claims) ctx, n, kind = _run(pcs._build_arguments_context(cid)) assert kind == "claims" assert n == 2 assert "טענה אחת" in ctx def test_block_zayin_claims_excluded(monkeypatch): """Decision-summary (block-zayin) claims are not original pleadings.""" cid = uuid4() async def _no_args(_case_id): return [] async def _claims(_case_id, *a, **k): return [ {"party_role": "appellant", "claim_text": "מקורית", "source_document": "appeal"}, {"party_role": "appellant", "claim_text": "מסיכום", "source_document": "block-zayin"}, ] monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _no_args) monkeypatch.setattr(pcs.db, "get_claims", _claims) ctx, n, kind = _run(pcs._build_arguments_context(cid)) assert kind == "claims" assert n == 1 assert "מקורית" in ctx and "מסיכום" not in ctx def test_none_when_no_source(monkeypatch): cid = uuid4() monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", lambda _c: _aw([])) monkeypatch.setattr(pcs.db, "get_claims", lambda _c, *a, **k: _aw([])) ctx, n, kind = _run(pcs._build_arguments_context(cid)) assert kind == "none" and n == 0 and ctx == "" async def _aw(v): return v # ── generation pins Opus 4.8 + high effort, grounded in claims ───── def test_summarize_pins_opus_and_high_effort(monkeypatch, tmp_path): cid = uuid4() captured: dict = {} async def _get_case(_case_id): return {"case_number": "8125-09-24", "title": "t", "appellants": ["א"], "respondents": ["ב"], "subject": "היטל השבחה", "property_address": "", "appeal_type": "betterment_levy", "practice_area": "betterment_levy"} async def _get_decision(_case_id): return None async def _args(_case_id): return [{"party": "appellant", "argument_title": "X", "argument_body": "Y", "legal_topic": ""}] async def _fake_query(prompt, *, timeout=None, model=None, effort=None, tools=None): captured["model"] = model captured["effort"] = effort captured["tools"] = tools captured["prompt"] = prompt return "## טענות העוררים\nתמצית." async def _style(_practice_area=""): return "(style)" monkeypatch.setattr(pcs.db, "get_case", _get_case) monkeypatch.setattr(pcs.db, "get_decision_by_case", _get_decision) monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _args) monkeypatch.setattr(pcs.block_writer, "_build_style_context", _style) monkeypatch.setattr(pcs.claude_session, "query", _fake_query) monkeypatch.setattr(pcs.config, "find_case_dir", lambda cn: tmp_path / cn) monkeypatch.setattr(pcs.config, "DATA_DIR", tmp_path) async def _mirror(*a, **k): return None monkeypatch.setattr(pcs.storage, "mirror", _mirror) monkeypatch.setattr(pcs.git_sync, "commit_and_push", lambda *a, **k: True) result = _run(pcs.summarize_party_claims(cid, "8125-09-24")) # The whole point of #202's generation-path constraint: assert captured["model"] == "claude-opus-4-8" assert captured["effort"] == "high" assert captured["tools"] == "" # prose, no tool_use # Grounded strictly in the case's own arguments (anti-hallucination). assert "אנטי-הזיה" in captured["prompt"] assert "Y" in captured["prompt"] # the actual argument body is in-context assert result["source_kind"] == "arguments" out = (tmp_path / "8125-09-24" / "documents" / "research" / "party-claims-summary.md") assert out.exists() assert "סיכום-מנהלים" in out.read_text(encoding="utf-8") def test_summarize_raises_when_no_claims(monkeypatch, tmp_path): cid = uuid4() monkeypatch.setattr(pcs.db, "get_case", lambda _c: _aw({"case_number": "9000-01-25"})) monkeypatch.setattr(pcs.db, "get_decision_by_case", lambda _c: _aw(None)) monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", lambda _c: _aw([])) monkeypatch.setattr(pcs.db, "get_claims", lambda _c, *a, **k: _aw([])) try: _run(pcs.summarize_party_claims(cid, "9000-01-25")) raise AssertionError("expected ValueError on empty source") except ValueError as e: assert "טענות" in str(e) # ── markdown → DOCX rendering uses template heading styles ───────── def test_render_markdown_body_maps_styles(): from docx import Document from legal_mcp.services import analysis_docx_exporter as ax doc = Document() md = ( "# כותרת ראשית\n\n" "---\n" "## טענות העוררים\n" "- טענה אחת\n" "1. סעיף ממוספר\n" "טקסט רגיל.\n" ) ax._render_markdown_body(doc, md) styles = [p.style.name for p in doc.paragraphs] texts = [p.text for p in doc.paragraphs] # HR is skipped; headings become Heading styles. assert "כותרת ראשית" in texts assert "Heading 1" in styles # # / ## → Heading 1 assert any("טענה אחת" in t for t in texts) assert not any(t.strip() in {"---", "#"} for t in texts) def test_next_version_is_prefix_scoped(tmp_path): from legal_mcp.services import analysis_docx_exporter as ax (tmp_path / "ניתוח-משפטי-v3.docx").write_bytes(b"x") # An analysis file at v3 must NOT bump the summary's version. assert ax._next_version(tmp_path, "סיכום-מנהלים-טענות") == 1 assert ax._next_version(tmp_path, "ניתוח-משפטי") == 4