Spaces:
Sleeping
Sleeping
| import json | |
| from unittest.mock import patch, MagicMock | |
| from lilith_agent.tools.ddg_search import web_search | |
| def test_web_search_returns_error_when_ddgs_hangs(monkeypatch): | |
| """If DDGS blocks forever, web_search must still return within seconds.""" | |
| import time as _time | |
| from lilith_agent.tools import ddg_search | |
| monkeypatch.setattr(ddg_search, "_HARD_TIMEOUT_S", 1) | |
| def _hang(*_a, **_kw): | |
| _time.sleep(30) | |
| return [] | |
| fake_ddgs = MagicMock() | |
| fake_ddgs.__enter__.return_value.text.side_effect = _hang | |
| fake_ddgs.__exit__.return_value = False | |
| with patch("ddgs.DDGS") as ddgs_cls: | |
| ddgs_cls.return_value = fake_ddgs | |
| t0 = _time.monotonic() | |
| result = web_search("moon", max_results=3) | |
| elapsed = _time.monotonic() - t0 | |
| assert elapsed < 3, f"web_search hung for {elapsed:.1f}s, hard timeout not enforced" | |
| assert "timed out" in result.lower() or "error" in result.lower() | |
| def test_web_search_passes_timeout(): | |
| fake_ddgs = MagicMock() | |
| fake_ddgs.__enter__.return_value.text.return_value = iter([]) | |
| fake_ddgs.__exit__.return_value = False | |
| with patch("ddgs.DDGS") as ddgs_cls: | |
| ddgs_cls.return_value = fake_ddgs | |
| web_search("hello", max_results=3) | |
| assert ddgs_cls.called, "DDGS must be constructed" | |
| _, kwargs = ddgs_cls.call_args | |
| assert "timeout" in kwargs and kwargs["timeout"] > 0, ( | |
| "web_search must pass a timeout to DDGS so a stalled network call fails fast" | |
| ) | |
| def test_setup_arize_skips_without_credentials(monkeypatch): | |
| from lilith_agent.observability import setup_arize | |
| monkeypatch.delenv("ARIZE_SPACE_ID", raising=False) | |
| monkeypatch.delenv("ARIZE_API_KEY", raising=False) | |
| assert setup_arize() is False | |
| def test_jsonl_callback_captures_chat_model_payload(tmp_path): | |
| from lilith_agent.observability import JsonlTraceCallback | |
| from langchain_core.messages import HumanMessage, SystemMessage | |
| cb = JsonlTraceCallback(tmp_path / "trace.jsonl") | |
| messages = [[SystemMessage(content="sys"), HumanMessage(content="hi")]] | |
| cb.on_chat_model_start({"name": "ChatAnthropic"}, messages, run_id="r0") | |
| events = [json.loads(line) for line in (tmp_path / "trace.jsonl").read_text().splitlines()] | |
| assert events[0]["event"] == "chat_model_start" | |
| assert events[0]["model"] == "ChatAnthropic" | |
| assert any(m["type"] == "system" and m["content"] == "sys" for m in events[0]["messages"][0]) | |
| assert any(m["type"] == "human" and m["content"] == "hi" for m in events[0]["messages"][0]) | |
| def test_jsonl_callback_captures_chain_boundaries(tmp_path): | |
| from lilith_agent.observability import JsonlTraceCallback | |
| cb = JsonlTraceCallback(tmp_path / "trace.jsonl") | |
| cb.on_chain_start({"name": "agent"}, {"messages": []}, run_id="c1") | |
| cb.on_chain_end({"messages": ["ok"]}, run_id="c1") | |
| events = [json.loads(line) for line in (tmp_path / "trace.jsonl").read_text().splitlines()] | |
| assert events[0]["event"] == "chain_start" | |
| assert events[0]["name"] == "agent" | |
| assert events[1]["event"] == "chain_end" | |
| assert events[1]["elapsed_s"] >= 0 | |
| def test_jsonl_callback_mirrors_non_error_events_at_debug(tmp_path, caplog): | |
| """Non-error LangGraph events are logged at DEBUG so they don't flood the console. | |
| Keeping INFO clean was the whole point of the level demotion — the full payload | |
| still lives in the .jsonl file, so the mirror is just a low-priority heartbeat. | |
| """ | |
| from lilith_agent.observability import JsonlTraceCallback | |
| import logging as _logging | |
| cb = JsonlTraceCallback(tmp_path / "trace.jsonl") | |
| with caplog.at_level(_logging.DEBUG, logger="lilith_agent.trace"): | |
| cb.on_tool_start({"name": "tavily_search"}, "moon", run_id="r1") | |
| matches = [ | |
| r for r in caplog.records | |
| if "tool_start" in r.getMessage() and "tavily_search" in r.getMessage() | |
| ] | |
| assert matches, "expected tool_start mirror record" | |
| assert all(r.levelno == _logging.DEBUG for r in matches) | |
| def test_jsonl_callback_mirrors_errors_at_warning(tmp_path, caplog): | |
| """Errors stay visible: mirrored at WARNING so they punch through the default cutoff.""" | |
| from lilith_agent.observability import JsonlTraceCallback | |
| import logging as _logging | |
| cb = JsonlTraceCallback(tmp_path / "trace.jsonl") | |
| with caplog.at_level(_logging.WARNING, logger="lilith_agent.trace"): | |
| cb.on_tool_error(RuntimeError("boom"), run_id="r1") | |
| matches = [r for r in caplog.records if "tool_error" in r.getMessage()] | |
| assert matches, "expected tool_error mirror record" | |
| assert all(r.levelno == _logging.WARNING for r in matches) | |
| def test_jsonl_callback_writes_tool_and_llm_events(tmp_path): | |
| from lilith_agent.observability import JsonlTraceCallback | |
| path = tmp_path / "trace.jsonl" | |
| cb = JsonlTraceCallback(path) | |
| cb.on_tool_start({"name": "web_search"}, "moon perigee", run_id="r1") | |
| cb.on_tool_end("results...", run_id="r1") | |
| cb.on_tool_error(RuntimeError("boom"), run_id="r2") | |
| lines = path.read_text().strip().splitlines() | |
| assert len(lines) == 3 | |
| events = [json.loads(line) for line in lines] | |
| assert events[0]["event"] == "tool_start" | |
| assert events[0]["name"] == "web_search" | |
| assert events[0]["input"] == "moon perigee" | |
| assert events[1]["event"] == "tool_end" | |
| assert events[2]["event"] == "tool_error" | |
| assert "boom" in events[2]["error"] | |