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
40 changes: 40 additions & 0 deletions tests/test_export_transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,43 @@ 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_headers_and_code_blocks():
mod = _load_export_module()
md = "# Title\n\n```py\nprint('<tag>')\n```\n\nBody **bold** and `inline`."
html = mod.md_to_html(md)

assert "<h1>Title</h1>" in html
assert "print('&lt;tag&gt;')" in html or "&lt;tag&gt;" in html
assert "<strong>bold</strong>" in html
assert "<code>inline</code>" in html


def test_md_to_html_blockquote_and_hr():
mod = _load_export_module()
md = "> Certified ruling\n\n---\n\nFinal paragraph."
html = mod.md_to_html(md)

assert "<blockquote>Certified ruling</blockquote>" in html
assert "<hr>" in html


def test_apply_personality_styling_votes_and_personalities():
mod = _load_export_module()
html = mod.apply_personality_styling(
"ARCHITECT votes YES. ENGINEER votes NO. SCRIBE records ABSTAIN."
)

assert 'class="p-architect"' in html
assert 'class="p-engineer"' in html
assert 'class="p-scribe"' in html
assert 'class="vote-yes"' in html
assert 'class="vote-no"' in html
assert 'class="vote-abstain"' in html


def test_exports_dir_resolves_to_portal_exports():
mod = _load_export_module()
expected = REPO_ROOT / "courtroom" / "portal" / "exports"
assert mod.EXPORTS_DIR == expected
70 changes: 70 additions & 0 deletions tests/test_litigation_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Regression tests for litigation LLM provider factory."""

from __future__ import annotations

import pytest

from litigation.providers.factory import 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://127.0.0.1:11434")
assert isinstance(provider, OllamaProvider)
assert provider.model == "llama3.2"


def test_get_provider_lm_studio() -> None:
provider = get_provider("lm_studio", "local-model")
assert isinstance(provider, OpenAICompatProvider)
assert provider.api_key == "lm-studio"
assert provider.model == "local-model"


def test_get_provider_openrouter_missing_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
with pytest.raises(ValueError, match="OPENROUTER_API_KEY"):
get_provider("openrouter", "anthropic/claude-3-haiku")


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",
"meta-llama/llama-3.2-3b-instruct",
openrouter_config={
"app_attribution": {
"http_referer": "https://example.com",
"x_title": "MORNINGSTAR",
},
"provider": {"sort": "price"},
"user": "court-session-1",
},
)
assert isinstance(provider, OpenAICompatProvider)
assert provider.api_key == "sk-or-test-key"
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_get_provider_openrouter_explicit_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
provider = get_provider("openrouter", "model", openrouter_api_key="explicit-key")
assert provider.api_key == "explicit-key"


def test_get_provider_unknown_raises() -> None:
with pytest.raises(ValueError, match="Unknown provider"):
get_provider("azure", "gpt-4")


def test_get_provider_normalizes_name() -> None:
provider = get_provider(" OLLAMA ", "llama3.2")
assert isinstance(provider, OllamaProvider)
93 changes: 93 additions & 0 deletions tests/test_litigation_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,96 @@ 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


def test_allocate_case_no_without_registry(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
missing_registry = tmp_path / "case-registry.yaml"
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", missing_registry)

case_no = litigation_run.allocate_case_no("DEL")

year = litigation_run.datetime.now().strftime("%Y")
assert case_no == f"{year}-DEL-001-001"
assert not missing_registry.exists()


def test_allocate_case_no_custom_category_and_deliberation(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
registry = tmp_path / "case-registry.yaml"
registry.write_text(
yaml.dump({"year": 2026, "categories": {"SECU": 3}}),
encoding="utf-8",
)
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry)

case_no = litigation_run.allocate_case_no("SECU", deliberation=2)

assert case_no == "2026-SECU-003-002"
data = yaml.safe_load(registry.read_text(encoding="utf-8"))
assert data["categories"]["SECU"] == 4


@pytest.mark.parametrize(
("raw", "expected"),
[
("Simple Matter", "simple-matter"),
("!!!", "matter"),
("a" * 80, "a" * 60),
(" Mixed Case #42 ", "mixed-case-42"),
],
)
def test_slugify(raw: str, expected: str) -> None:
assert litigation_run.slugify(raw) == expected


def test_save_transcript_courtroom_location(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litigation_run, "REPO_ROOT", tmp_path)
missing_registry = tmp_path / "courtroom" / "case-registry.yaml"
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", missing_registry)

path = litigation_run.save_transcript("court matter", "Deliberation body.", location="courtroom")

assert path.parent == tmp_path / "courtroom" / "transcripts"
assert path.exists()


def test_save_transcript_appends_scribe_certification(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litigation_run, "REPO_ROOT", tmp_path)
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", tmp_path / "missing-registry.yaml")

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", "Body without footer.")
content = path.read_text(encoding="utf-8")

assert "> *Transcript certified by MORNINGSTAR::SCRIBE*" in content


def test_save_transcript_truncates_long_matter_title(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litigation_run, "REPO_ROOT", tmp_path)
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", tmp_path / "missing-registry.yaml")

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"))

long_matter = "x" * 100
path = litigation_run.save_transcript(long_matter, "Body.")
content = path.read_text(encoding="utf-8")

assert "# Transcript: In Re: " + ("x" * 80) + "..." in content
33 changes: 33 additions & 0 deletions tests/test_portal_generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Regression tests for courtroom portal generate.py path resolution."""

from __future__ import annotations

import importlib.util
from pathlib import Path

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_generate_base_dir_is_courtroom():
mod = _load_generate_module()
expected = REPO_ROOT / "courtroom"
assert mod.BASE_DIR == expected


def test_generate_transcript_index_uses_courtroom_transcripts():
"""generate.py must read courtroom/transcripts, not courtroom/courtroom/transcripts."""
mod = _load_generate_module()
transcripts_dir = mod.BASE_DIR / "transcripts"
expected = REPO_ROOT / "courtroom" / "transcripts"
assert transcripts_dir == expected
assert "courtroom/courtroom" not in str(transcripts_dir)
assert transcripts_dir.exists()
18 changes: 18 additions & 0 deletions tests/test_portal_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,21 @@ def test_viewer_path_served_over_http():
# Browser resolves relative to /courtroom/portal/viewer.html -> /courtroom/transcripts/
assert rel == "../transcripts/"
assert "courtroom/courtroom" not in rel


def test_launch_sh_exports_dir():
"""launch.sh must export HTML to portal/exports, not a stale project-root path."""
text = LAUNCH_SH.read_text(encoding="utf-8")
assert 'EXPORTS_DIR="$SCRIPT_DIR/exports"' in text
assert "$PROJECT_ROOT/portal/exports" not in text


def test_viewer_html_sessions_fetch_path():
"""viewer.html sessions path from portal/ must resolve to courtroom/sessions/."""
text = VIEWER_HTML.read_text(encoding="utf-8")
m = re.search(r"const SESSIONS_DIR = '([^']+)';", text)
assert m is not None
rel = m.group(1)
resolved = (PORTAL_DIR / rel).resolve()
expected = (REPO_ROOT / "courtroom" / "sessions").resolve()
assert resolved == expected
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 auth module?")
assert "Should we refactor the auth module?" in user_prompt
assert "**MATTER:**" in user_prompt


def test_build_deliberation_prompts_hearing_type_instructions() -> None:
from litigation.prompts.assembler import build_deliberation_prompts

_, standard = build_deliberation_prompts("matter", hearing_type="standard")
_, expedited = build_deliberation_prompts("matter", hearing_type="expedited")
_, contempt = build_deliberation_prompts("matter", hearing_type="contempt")

assert "Standard Deliberation Flow" in standard
assert "EXPEDITED format" in expedited
assert "CONTEMPT HEARING" in contempt


def test_build_deliberation_prompts_excludes_spectators() -> None:
from litigation.prompts.assembler import build_deliberation_prompts

system_with, _ = build_deliberation_prompts("matter", include_spectators=True)
system_without, _ = build_deliberation_prompts("matter", include_spectators=False)

if "## Spectators (Optional Commentary)" in system_with:
assert "## Spectators (Optional Commentary)" not in system_without