Additional tests

This commit is contained in:
Yiorgis Gozadinos 2026-02-13 18:08:03 +02:00
parent 84c48e1541
commit 72c8f6e1b1
No known key found for this signature in database
9 changed files with 440 additions and 6 deletions

View file

@ -8,7 +8,7 @@ from io import StringIO
from typing import Any
def build_namespace(
def build_namespace( # pragma: no cover
client: Any, config: Any, context: Any, loop: asyncio.AbstractEventLoop
) -> dict[str, Any]:
"""Build execution namespace with haiku.rag functions injected."""
@ -134,7 +134,7 @@ def send_response(result: dict[str, Any]) -> None:
sys.stdout.flush()
async def main() -> None:
async def main() -> None: # pragma: no cover
"""Main entry point for container execution.
Runs a loop reading length-prefixed JSON messages and executing code.

View file

@ -34,7 +34,7 @@ try:
logfire.configure(send_to_logfire="if-token-present", console=False)
logfire.instrument_pydantic_ai()
except ImportError:
except ImportError: # pragma: no cover
pass
try:
@ -47,7 +47,7 @@ try:
from haiku.rag.chat.widgets.chat_history import ChatHistory, CitationWidget
TEXTUAL_AVAILABLE = True
except ImportError:
except ImportError: # pragma: no cover
TEXTUAL_AVAILABLE = False
App = object # type: ignore
SystemCommand = object # type: ignore

View file

@ -21,7 +21,7 @@ try:
from haiku.rag.inspector.widgets.search_modal import SearchModal
TEXTUAL_AVAILABLE = True
except ImportError:
except ImportError: # pragma: no cover
TEXTUAL_AVAILABLE = False
App = object # type: ignore

View file

@ -468,7 +468,7 @@ async def is_up_to_date() -> tuple[bool, Version, Version]:
response = await client.get("https://pypi.org/pypi/haiku.rag/json")
data = response.json()
pypi_version = parse(data["info"]["version"])
except Exception:
except Exception: # pragma: no cover
# If no network connection, do not raise alarms.
pypi_version = running_version
return running_version >= pypi_version, running_version, pypi_version

View file

@ -128,3 +128,14 @@ markers = [
# pyproject.toml
filterwarnings = ["error", "ignore::UserWarning", "ignore::DeprecationWarning"]
[tool.coverage.run]
source = ["haiku_rag_slim"]
[tool.coverage.report]
show_missing = true
exclude_also = [
"if TYPE_CHECKING:",
"@abstractmethod",
"raise NotImplementedError",
]

View file

@ -0,0 +1,50 @@
import json
from io import StringIO
from haiku.rag.agents.rlm.runner import execute_code, send_response
def test_execute_code_success():
namespace: dict = {}
result = execute_code("x = 1 + 1", namespace, max_output_chars=1000)
assert result["success"] is True
assert result["stderr"] == ""
def test_execute_code_stdout_capture():
namespace: dict = {}
result = execute_code("print('hello')", namespace, max_output_chars=1000)
assert result["success"] is True
assert "hello" in result["stdout"]
def test_execute_code_exception():
namespace: dict = {}
result = execute_code("raise ValueError('boom')", namespace, max_output_chars=1000)
assert result["success"] is False
assert "ValueError" in result["stderr"]
assert "boom" in result["stderr"]
def test_execute_code_output_truncation():
namespace: dict = {}
code = "print('x' * 100)"
result = execute_code(code, namespace, max_output_chars=10)
assert result["success"] is True
assert "truncated" in result["stdout"]
assert len(result["stdout"]) < 100
def test_send_response(monkeypatch):
buf = StringIO()
monkeypatch.setattr("sys.stdout", buf)
payload = {"success": True, "stdout": "hi", "stderr": ""}
send_response(payload)
output = buf.getvalue()
lines = output.split("\n", 1)
length = int(lines[0])
body = lines[1]
assert json.loads(body) == payload
assert length == len(json.dumps(payload))

38
tests/test_logging.py Normal file
View file

@ -0,0 +1,38 @@
import logging
from haiku.rag.logging import configure_cli_logging, get_logger
def test_get_logger():
logger = get_logger()
assert logger.name == "haiku.rag"
assert logger.level == logging.INFO
assert len(logger.handlers) == 1
assert logger.propagate is False
def test_get_logger_idempotent():
logger1 = get_logger()
logger2 = get_logger()
assert logger1 is logger2
assert len(logger2.handlers) == 1
def test_configure_cli_logging():
logger = configure_cli_logging()
assert logger.name == "haiku.rag"
assert logger.propagate is False
root = logging.getLogger()
assert root.level == logging.ERROR
assert len(root.handlers) == 0
for name in ("httpx", "httpcore", "docling", "urllib3", "asyncio"):
noisy = logging.getLogger(name)
assert noisy.level == logging.ERROR
assert noisy.propagate is False
def test_configure_cli_logging_custom_level():
logger = configure_cli_logging(level=logging.DEBUG)
assert logger.level == logging.DEBUG

View file

@ -318,3 +318,285 @@ def test_get_package_versions():
for key, value in versions.items():
assert isinstance(value, str)
assert len(value) > 0
# --- parse_datetime tests ---
def test_parse_datetime_iso8601():
from haiku.rag.utils import parse_datetime
dt = parse_datetime("2025-01-15T14:30:00")
assert dt.year == 2025
assert dt.month == 1
assert dt.day == 15
assert dt.hour == 14
assert dt.minute == 30
def test_parse_datetime_date_only():
from haiku.rag.utils import parse_datetime
dt = parse_datetime("2025-01-15")
assert dt.year == 2025
assert dt.month == 1
assert dt.day == 15
def test_parse_datetime_with_timezone():
from haiku.rag.utils import parse_datetime
dt = parse_datetime("2025-01-15T14:30:00+00:00")
assert dt.year == 2025
assert dt.tzinfo is not None
def test_parse_datetime_invalid():
from haiku.rag.utils import parse_datetime
with pytest.raises(ValueError, match="Could not parse datetime"):
parse_datetime("not-a-date")
# --- to_utc tests ---
def test_to_utc_naive_datetime():
from datetime import datetime
from haiku.rag.utils import to_utc
naive = datetime(2025, 6, 15, 12, 0, 0)
result = to_utc(naive)
assert result.tzinfo is not None
def test_to_utc_utc_datetime():
from datetime import UTC, datetime
from haiku.rag.utils import to_utc
utc_dt = datetime(2025, 6, 15, 12, 0, 0, tzinfo=UTC)
result = to_utc(utc_dt)
assert result is utc_dt
def test_to_utc_aware_non_utc():
from datetime import UTC, datetime, timedelta, timezone
from haiku.rag.utils import to_utc
eastern = timezone(timedelta(hours=-5))
aware = datetime(2025, 6, 15, 12, 0, 0, tzinfo=eastern)
result = to_utc(aware)
assert result.tzinfo == UTC
assert result.hour == 17
# --- apply_common_settings tests ---
def test_apply_common_settings_no_settings():
from haiku.rag.config.models import ModelConfig
from haiku.rag.utils import apply_common_settings
mc = ModelConfig(provider="openai", name="gpt-4o")
result = apply_common_settings(None, dict, mc)
assert result is None
def test_apply_common_settings_temperature():
from haiku.rag.config.models import ModelConfig
from haiku.rag.utils import apply_common_settings
mc = ModelConfig(provider="openai", name="gpt-4o", temperature=0.7)
result = apply_common_settings(None, dict, mc)
assert result is not None
assert result["temperature"] == 0.7
def test_apply_common_settings_max_tokens():
from haiku.rag.config.models import ModelConfig
from haiku.rag.utils import apply_common_settings
mc = ModelConfig(provider="openai", name="gpt-4o", max_tokens=500)
result = apply_common_settings(None, dict, mc)
assert result is not None
assert result["max_tokens"] == 500
def test_apply_common_settings_existing():
from haiku.rag.config.models import ModelConfig
from haiku.rag.utils import apply_common_settings
mc = ModelConfig(provider="openai", name="gpt-4o", temperature=0.5)
existing = {"some_key": "value"}
result = apply_common_settings(existing, dict, mc)
assert result is not None
assert result["temperature"] == 0.5
assert result["some_key"] == "value"
# --- format_bytes tests ---
def test_format_bytes():
from haiku.rag.utils import format_bytes
assert format_bytes(0) == "0.0 B"
assert format_bytes(512) == "512.0 B"
assert format_bytes(1024) == "1.0 KB"
assert format_bytes(1048576) == "1.0 MB"
assert format_bytes(1073741824) == "1.0 GB"
assert format_bytes(1099511627776) == "1.0 TB"
assert format_bytes(1125899906842624) == "1.0 PB"
# --- format_citations tests ---
def test_format_citations_empty():
from haiku.rag.utils import format_citations
assert format_citations([]) == ""
def test_format_citations_with_citation():
from haiku.rag.agents.research.models import Citation
from haiku.rag.utils import format_citations
citation = Citation(
document_id="doc1",
chunk_id="chunk1",
document_uri="test://doc",
document_title="Test Doc",
content="Some content",
page_numbers=[1],
headings=["Intro"],
)
result = format_citations([citation])
assert "[doc1:chunk1]" in result
assert "Test Doc" in result
assert "p. 1" in result
assert "Section: Intro" in result
assert "Some content" in result
def test_format_citations_multiple_pages():
from haiku.rag.agents.research.models import Citation
from haiku.rag.utils import format_citations
citation = Citation(
document_id="doc1",
chunk_id="chunk1",
document_uri="test://doc",
content="Content",
page_numbers=[1, 2, 3],
)
result = format_citations([citation])
assert "pp. 1-3" in result
def test_format_citations_no_title():
from haiku.rag.agents.research.models import Citation
from haiku.rag.utils import format_citations
citation = Citation(
document_id="doc1",
chunk_id="chunk1",
document_uri="test://doc",
content="Content",
)
result = format_citations([citation])
assert "test://doc" in result
# --- format_citations_rich tests ---
def test_format_citations_rich_empty():
from haiku.rag.utils import format_citations_rich
assert format_citations_rich([]) == []
def test_format_citations_rich_with_citation():
from rich.panel import Panel
from rich.text import Text
from haiku.rag.agents.research.models import Citation
from haiku.rag.utils import format_citations_rich
citation = Citation(
document_id="doc1",
chunk_id="chunk1",
document_uri="test://doc",
document_title="Test Doc",
content="Some content",
page_numbers=[1, 2],
headings=["Intro"],
)
result = format_citations_rich([citation])
assert len(result) == 2
assert isinstance(result[0], Text)
assert isinstance(result[1], Panel)
# --- get_default_data_dir tests ---
def test_get_default_data_dir():
from pathlib import Path
from haiku.rag.utils import get_default_data_dir
result = get_default_data_dir()
assert isinstance(result, Path)
assert "haiku.rag" in str(result)
# --- build_prompt tests ---
def test_build_prompt_without_preamble():
from haiku.rag.config.models import AppConfig
from haiku.rag.utils import build_prompt
config = AppConfig()
result = build_prompt("Base prompt", config)
assert result == "Base prompt"
def test_build_prompt_with_preamble():
from haiku.rag.config.models import AppConfig, PromptsConfig
from haiku.rag.utils import build_prompt
config = AppConfig(prompts=PromptsConfig(domain_preamble="You are a legal expert."))
result = build_prompt("Base prompt", config)
assert result == "You are a legal expert.\n\nBase prompt"
# --- is_up_to_date tests ---
@pytest.mark.asyncio
async def test_is_up_to_date(monkeypatch):
from unittest.mock import AsyncMock, MagicMock
import httpx
from haiku.rag.utils import is_up_to_date
mock_response = MagicMock()
mock_response.json.return_value = {"info": {"version": "0.0.1"}}
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
monkeypatch.setattr(httpx, "AsyncClient", lambda: mock_client)
is_current, running, latest = await is_up_to_date()
assert is_current is True
assert running >= latest

View file

@ -0,0 +1,53 @@
from haiku.rag.tools.prompts import build_tools_prompt
def test_empty_features():
result = build_tools_prompt([])
assert result == ""
def test_single_feature_search():
result = build_tools_prompt(["search"])
assert "search" in result
assert "document_name" in result.lower()
def test_single_feature_documents():
result = build_tools_prompt(["documents"])
assert "list_documents" in result
assert "summarize_document" in result
assert "get_document" in result
def test_single_feature_qa():
result = build_tools_prompt(["qa"])
assert "ask" in result
assert "document_name" in result.lower()
def test_single_feature_analysis():
result = build_tools_prompt(["analysis"])
assert "analyze" in result
def test_multiple_features():
result = build_tools_prompt(["search", "qa", "documents"])
assert "search" in result
assert "ask" in result
assert "list_documents" in result
def test_search_and_qa_both_add_document_name_examples():
result = build_tools_prompt(["search", "qa"])
assert "search for embeddings" in result.lower() or "embeddings" in result
assert "what does the ML paper say" in result or "ML paper" in result
def test_unknown_features_ignored():
result = build_tools_prompt(["nonexistent", "also_fake"])
assert result == ""
def test_unknown_mixed_with_known():
result = build_tools_prompt(["nonexistent", "search"])
assert "search" in result