Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions tests/test_export_transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<h2>Section</h2>" in html
assert "<strong>Bold</strong>" in html


def test_apply_personality_styling_wraps_votes():
mod = _load_export_module()
styled = mod.apply_personality_styling("<p>Vote: YES and NO and ABSTAIN</p>")
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"
71 changes: 71 additions & 0 deletions tests/test_litigation_providers.py
Original file line number Diff line number Diff line change
@@ -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")
57 changes: 57 additions & 0 deletions tests/test_litigation_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
68 changes: 68 additions & 0 deletions tests/test_portal_generate.py
Original file line number Diff line number Diff line change
@@ -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("<html><head></head><body></body></html>", 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 "</head>" 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(
"<p>MORNINGSTAR ruled YES while Architect voted NO and Scribe recorded ABSTAIN.</p>",
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
30 changes: 30 additions & 0 deletions tests/test_prompt_assembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions tests/test_research_report.py
Original file line number Diff line number Diff line change
@@ -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("<div")
assert "Plain report text" in wrapped


def test_ensure_html_wrapped_adds_style_div_for_fragment() -> None:
wrapped = research_report._ensure_html_wrapped("<h2>Title</h2><p>Body</p>")
assert "font-family" in wrapped
assert "<h2>Title</h2>" in wrapped


def test_ensure_html_wrapped_leaves_full_document_unchanged() -> None:
html = "<html><body><p>Full doc</p></body></html>"
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", "<p>HTML</p>", "Topic")

content = path.read_text(encoding="utf-8")
assert path.name == "RPT_ABCD1234_20260701_120000.html"
assert "<!-- Report ID: RPT_ABCD1234" in content
assert "<p>HTML</p>" in content
75 changes: 75 additions & 0 deletions tests/test_workflows_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Regression tests for research workflow web search helpers."""

from __future__ import annotations

import pytest

from agents.workflows.search import SearchResult, _domain_from_url, search_web


@pytest.mark.parametrize(
("url", "expected"),
[
("https://www.bbc.com/news/article", "bbc.com"),
("https://reuters.com/world", "reuters.com"),
("", "unknown"),
("not-a-url", "unknown"),
],
)
def test_domain_from_url(url: str, expected: str) -> None:
assert _domain_from_url(url) == expected


def test_search_web_uses_tavily_when_key_set(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("TAVILY_API_KEY", "tvly-test")

class FakeClient:
def __init__(self, api_key: str) -> None:
assert api_key == "tvly-test"

def search(self, query: str, max_results: int = 10) -> dict:
return {
"results": [
{
"title": "Example",
"url": "https://example.com/page",
"content": "Snippet text",
}
]
}

monkeypatch.setitem(__import__("sys").modules, "tavily", type("m", (), {"TavilyClient": FakeClient})())

results, backend = search_web("test query", max_results=5)
assert backend == "tavily"
assert len(results) == 1
assert results[0] == SearchResult(
title="Example",
url="https://example.com/page",
snippet="Snippet text",
source="example.com",
)


def test_search_web_falls_back_to_duckduckgo(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("TAVILY_API_KEY", raising=False)

class FakeDDGS:
def text(self, query: str, max_results: int = 10):
yield {
"title": "DDG Result",
"href": "https://duck.com/result",
"body": "Body text",
}

monkeypatch.setitem(
__import__("sys").modules,
"duckduckgo_search",
type("m", (), {"DDGS": FakeDDGS})(),
)

results, backend = search_web("fallback query")
assert backend == "duckduckgo"
assert len(results) == 1
assert results[0].title == "DDG Result"
assert results[0].source == "duck.com"