diff --git a/tests/test_export_transcript.py b/tests/test_export_transcript.py index a65b141..ddc30b9 100644 --- a/tests/test_export_transcript.py +++ b/tests/test_export_transcript.py @@ -36,3 +36,30 @@ def test_extract_title_from_dated_filename(): mod = _load_export_module() path = Path("2026-02-15-framework-enhancement-analysis.md") assert mod.extract_title(path, "") == "Framework Enhancement Analysis" + + +def test_md_to_html_converts_headers_and_bold(): + mod = _load_export_module() + html = mod.md_to_html("## Section\n\n**Bold** claim.") + assert "

Section

" in html + assert "Bold" in html + + +def test_apply_personality_styling_wraps_votes(): + mod = _load_export_module() + styled = mod.apply_personality_styling("

Vote: YES and NO and ABSTAIN

") + assert 'class="vote-yes"' in styled + assert 'class="vote-no"' in styled + assert 'class="vote-abstain"' in styled + + +def test_exports_dir_resolves_under_portal(): + mod = _load_export_module() + expected = REPO_ROOT / "courtroom" / "portal" / "exports" + assert mod.EXPORTS_DIR == expected + + +def test_extract_title_from_timestamped_filename(): + mod = _load_export_module() + path = Path("20260215_120000_framework_review.md") + assert mod.extract_title(path, "") == "Framework Review" diff --git a/tests/test_litigation_providers.py b/tests/test_litigation_providers.py new file mode 100644 index 0000000..672876f --- /dev/null +++ b/tests/test_litigation_providers.py @@ -0,0 +1,71 @@ +"""Regression tests for litigation LLM provider factory.""" + +from __future__ import annotations + +import pytest + +from litigation.providers.factory import ( + _openrouter_extra_body, + _openrouter_headers, + get_provider, +) +from litigation.providers.ollama_provider import OllamaProvider +from litigation.providers.openai_compat_provider import OpenAICompatProvider + + +def test_get_provider_ollama() -> None: + provider = get_provider("ollama", "llama3.2", ollama_base_url="http://localhost:11434") + assert isinstance(provider, OllamaProvider) + assert provider.model == "llama3.2" + assert provider.base_url == "http://localhost:11434" + + +def test_get_provider_lm_studio() -> None: + provider = get_provider("lm_studio", "local-model", lm_studio_base_url="http://localhost:1234/v1") + assert isinstance(provider, OpenAICompatProvider) + assert provider.model == "local-model" + assert provider.base_url == "http://localhost:1234/v1" + assert provider.api_key == "lm-studio" + + +def test_get_provider_openrouter_requires_api_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + with pytest.raises(ValueError, match="OPENROUTER_API_KEY"): + get_provider("openrouter", "openai/gpt-4o-mini") + + +def test_get_provider_openrouter_with_env_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test-key") + provider = get_provider("openrouter", "openai/gpt-4o-mini") + assert isinstance(provider, OpenAICompatProvider) + assert provider.api_key == "sk-or-test-key" + assert provider.base_url == "https://openrouter.ai/api/v1" + + +def test_get_provider_openrouter_headers_and_extra_body(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test-key") + cfg = { + "app_attribution": {"http_referer": "https://example.com", "x_title": "MORNINGSTAR"}, + "provider": {"sort": "price"}, + "user": "court-session-1", + } + provider = get_provider("openrouter", "model", openrouter_config=cfg) + assert provider.default_headers == { + "HTTP-Referer": "https://example.com", + "X-Title": "MORNINGSTAR", + } + assert provider.extra_body == {"provider": {"sort": "price"}, "user": "court-session-1"} + + +def test_openrouter_headers_skips_non_dict_attribution() -> None: + assert _openrouter_headers({"app_attribution": "invalid"}) == {} + + +def test_openrouter_extra_body_empty_when_no_prefs() -> None: + assert _openrouter_extra_body({}) == {} + assert _openrouter_extra_body({"provider": "not-a-dict"}) == {} + + +def test_get_provider_unknown_raises() -> None: + with pytest.raises(ValueError, match="Unknown provider"): + get_provider("anthropic", "claude-3") diff --git a/tests/test_litigation_run.py b/tests/test_litigation_run.py index 8e46431..3d1a17a 100644 --- a/tests/test_litigation_run.py +++ b/tests/test_litigation_run.py @@ -76,3 +76,60 @@ def test_save_transcript_filename_suffix_independent_of_case_no( assert path.name.endswith("-1.md") assert "**Case No.:** 2026-DEL-020-001" in content + + +@pytest.mark.parametrize( + ("raw", "expected"), + [ + ("Simple Matter", "simple-matter"), + (" Mixed CASE 123!! ", "mixed-case-123"), + ("", "matter"), + ("---", "matter"), + ], +) +def test_slugify(raw: str, expected: str) -> None: + assert litigation_run.slugify(raw) == expected + + +def test_allocate_case_no_without_registry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + missing = tmp_path / "no-registry.yaml" + monkeypatch.setattr(litigation_run, "REGISTRY_PATH", missing) + + case_no = litigation_run.allocate_case_no("DEL", deliberation=2) + + assert case_no.startswith(litigation_run.datetime.now().strftime("%Y") + "-DEL-001-002") + assert not missing.exists() + + +def test_save_transcript_courtroom_location(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + registry = tmp_path / "case-registry.yaml" + registry.write_text(yaml.dump({"year": 2026, "categories": {"DEL": 1}}), encoding="utf-8") + monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry) + monkeypatch.setattr(litigation_run, "REPO_ROOT", tmp_path) + + courtroom_dir = tmp_path / "courtroom" / "transcripts" + courtroom_dir.mkdir(parents=True) + + path = litigation_run.save_transcript("courtroom matter", "Body.", location="courtroom") + + assert path.parent == courtroom_dir + assert "courtroom matter" in path.read_text(encoding="utf-8") + + +def test_save_transcript_appends_scribe_certification( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + registry = tmp_path / "case-registry.yaml" + registry.write_text(yaml.dump({"year": 2026, "categories": {"DEL": 1}}), encoding="utf-8") + monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry) + + fake_module = tmp_path / "litigation_pkg" + fake_module.mkdir() + (fake_module / "run.py").write_text("", encoding="utf-8") + (fake_module / "transcripts").mkdir() + monkeypatch.setattr(litigation_run, "__file__", str(fake_module / "run.py")) + + path = litigation_run.save_transcript("cert test", "Deliberation without footer.") + content = path.read_text(encoding="utf-8") + + assert content.endswith("> *Transcript certified by MORNINGSTAR::SCRIBE*\n") diff --git a/tests/test_portal_generate.py b/tests/test_portal_generate.py new file mode 100644 index 0000000..3d2e837 --- /dev/null +++ b/tests/test_portal_generate.py @@ -0,0 +1,68 @@ +"""Regression tests for courtroom portal generate post-processing.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +GENERATE_SCRIPT = REPO_ROOT / "courtroom" / "portal" / "generate.py" + + +def _load_generate_module(): + spec = importlib.util.spec_from_file_location("portal_generate", GENERATE_SCRIPT) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_inject_courtroom_css(tmp_path: Path) -> None: + mod = _load_generate_module() + html_file = tmp_path / "page.html" + html_file.write_text("", encoding="utf-8") + + injected = mod.inject_courtroom_css(tmp_path) + + content = html_file.read_text(encoding="utf-8") + assert injected == 1 + assert "MORNINGSTAR Courtroom Portal" in content + assert "" in content + + +def test_apply_personality_styling(tmp_path: Path) -> None: + mod = _load_generate_module() + transcript_dir = tmp_path / "courtroom" / "transcripts" + transcript_dir.mkdir(parents=True) + html_file = transcript_dir / "sample.html" + html_file.write_text( + "

MORNINGSTAR ruled YES while Architect voted NO and Scribe recorded ABSTAIN.

", + encoding="utf-8", + ) + + styled = mod.apply_personality_styling(tmp_path) + + content = html_file.read_text(encoding="utf-8") + assert styled == 1 + assert 'class="p-morningstar"' in content + assert 'class="vote-yes"' in content + assert 'class="vote-no"' in content + assert 'class="vote-abstain"' in content + + +def test_generate_transcript_index(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + mod = _load_generate_module() + transcripts_dir = tmp_path / "transcripts" + transcripts_dir.mkdir() + (transcripts_dir / "20260215_120000_framework_review.md").write_text("# Review\n", encoding="utf-8") + (transcripts_dir / "README.md").write_text("ignore\n", encoding="utf-8") + monkeypatch.setattr(mod, "BASE_DIR", tmp_path) + + index_path = mod.generate_transcript_index(tmp_path) + + assert index_path is not None + html = Path(index_path).read_text(encoding="utf-8") + assert "Framework Review" in html + assert "courtroom/transcripts/20260215_120000_framework_review.html" in html diff --git a/tests/test_prompt_assembler.py b/tests/test_prompt_assembler.py index 24b2938..3591f4e 100644 --- a/tests/test_prompt_assembler.py +++ b/tests/test_prompt_assembler.py @@ -20,3 +20,33 @@ ) def test_parse_feasibility(raw: str, expected: int) -> None: assert _parse_feasibility(raw) == expected + + +def test_build_deliberation_prompts_includes_matter() -> None: + from litigation.prompts.assembler import build_deliberation_prompts + + _, user_prompt = build_deliberation_prompts("Should we refactor the parser?", feasibility="F3") + assert "Should we refactor the parser?" in user_prompt + assert "**Feasibility:** F3" in user_prompt + + +@pytest.mark.parametrize("hearing_type", ["expedited", "special_inquiry", "contempt"]) +def test_build_deliberation_prompts_hearing_types(hearing_type: str) -> None: + from litigation.prompts.assembler import build_deliberation_prompts + + system_prompt, user_prompt = build_deliberation_prompts( + "Hearing type matter", + hearing_type=hearing_type, + ) + assert "Hearing type matter" in user_prompt + assert len(system_prompt) > 100 + + +def test_build_deliberation_prompts_excludes_spectators_when_disabled() -> None: + from litigation.prompts.assembler import build_deliberation_prompts + + with_spectators, _ = build_deliberation_prompts("Matter", include_spectators=True) + without_spectators, _ = build_deliberation_prompts("Matter", include_spectators=False) + + assert "## Spectators (Optional Commentary)" in with_spectators + assert "## Spectators (Optional Commentary)" not in without_spectators diff --git a/tests/test_research_report.py b/tests/test_research_report.py new file mode 100644 index 0000000..998e32f --- /dev/null +++ b/tests/test_research_report.py @@ -0,0 +1,44 @@ +"""Regression tests for research report workflow helpers.""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from agents.workflows import research_report + + +def test_generate_report_id_format() -> None: + report_id = research_report._generate_report_id() + assert re.fullmatch(r"RPT_[A-Z0-9]{8}", report_id) + + +def test_ensure_html_wrapped_wraps_plain_text() -> None: + wrapped = research_report._ensure_html_wrapped("Plain report text") + assert wrapped.startswith(" None: + wrapped = research_report._ensure_html_wrapped("

Title

Body

") + assert "font-family" in wrapped + assert "

Title

" in wrapped + + +def test_ensure_html_wrapped_leaves_full_document_unchanged() -> None: + html = "

Full doc

" + assert research_report._ensure_html_wrapped(html) == html + + +def test_save_report_writes_header_and_html(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + reports_dir = tmp_path / "reports" + monkeypatch.setattr(research_report, "REPORTS_DIR", reports_dir) + + path = research_report.save_report("RPT_ABCD1234", "20260701_120000", "

HTML

", "Topic") + + content = path.read_text(encoding="utf-8") + assert path.name == "RPT_ABCD1234_20260701_120000.html" + assert "