Additional tests
This commit is contained in:
parent
84c48e1541
commit
72c8f6e1b1
9 changed files with 440 additions and 6 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
50
tests/agents/rlm/test_runner.py
Normal file
50
tests/agents/rlm/test_runner.py
Normal 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
38
tests/test_logging.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
53
tests/tools/test_prompts.py
Normal file
53
tests/tools/test_prompts.py
Normal 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
|
||||
Loading…
Reference in a new issue