"""Tests for graphify.querylog.""" import json import os import pytest from pathlib import Path from graphify.querylog import log_query, nodes_from_result # --------------------------------------------------------------------------- # nodes_from_result # --------------------------------------------------------------------------- def test_nodes_from_result_parses_header(): result = "Traversal: BFS depth=2 | Start: ['foo'] | 7 nodes found\n\nNODE foo" assert nodes_from_result(result) == 7 def test_nodes_from_result_singular(): assert nodes_from_result("1 node found") == 1 def test_nodes_from_result_missing(): assert nodes_from_result("no match here") is None def test_nodes_from_result_empty(): assert nodes_from_result("") is None # --------------------------------------------------------------------------- # log_query — basic write # --------------------------------------------------------------------------- def test_log_query_writes_jsonl(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) log_query(kind="query", question="what is X", corpus="/some/graph.json", result="3 nodes found\nNODE a", duration_ms=12.5, mode="bfs", depth=2) lines = log_file.read_text().splitlines() assert len(lines) == 1 rec = json.loads(lines[0]) assert rec["kind"] == "query" assert rec["question"] == "what is X" assert rec["corpus"] == "/some/graph.json" assert rec["nodes_returned"] == 3 assert rec["result_chars"] > 0 assert rec["duration_ms"] == pytest.approx(12.5, abs=0.01) assert rec["mode"] == "bfs" assert "ts" in rec def test_log_query_appends(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) log_query(kind="query", question="q1", corpus="/g.json") log_query(kind="query", question="q2", corpus="/g.json") lines = log_file.read_text().splitlines() assert len(lines) == 2 assert json.loads(lines[0])["question"] == "q1" assert json.loads(lines[1])["question"] == "q2" # --------------------------------------------------------------------------- # opt-out / opt-in # --------------------------------------------------------------------------- def test_disable_env(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.setenv("GRAPHIFY_QUERY_LOG_DISABLE", "1") log_query(kind="query", question="q", corpus="/g.json") assert not log_file.exists() def test_disable_env_true(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.setenv("GRAPHIFY_QUERY_LOG_DISABLE", "true") log_query(kind="query", question="q", corpus="/g.json") assert not log_file.exists() def test_responses_not_logged_by_default(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_RESPONSES", raising=False) log_query(kind="query", question="q", corpus="/g.json", result="NODE foo") rec = json.loads(log_file.read_text()) assert "response" not in rec def test_responses_optin(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.setenv("GRAPHIFY_QUERY_LOG_RESPONSES", "1") monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) log_query(kind="query", question="q", corpus="/g.json", result="NODE foo bar") rec = json.loads(log_file.read_text()) assert rec["response"] == "NODE foo bar" # --------------------------------------------------------------------------- # robustness — never raises # --------------------------------------------------------------------------- def test_log_never_raises(tmp_path, monkeypatch): # Point at a directory — open() for append will fail bad_path = tmp_path / "is_a_dir" bad_path.mkdir() monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(bad_path)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) # Must not raise log_query(kind="query", question="q", corpus="/g.json") def test_log_creates_parent_dirs(tmp_path, monkeypatch): log_file = tmp_path / "deep" / "nested" / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) log_query(kind="query", question="q", corpus="/g.json") assert log_file.exists() # --------------------------------------------------------------------------- # field coverage # --------------------------------------------------------------------------- def test_nodes_returned_inferred_from_result(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) log_query(kind="query", question="q", corpus="/g.json", result="5 nodes found\nNODE a\nNODE b") rec = json.loads(log_file.read_text()) assert rec["nodes_returned"] == 5 def test_explicit_nodes_returned_takes_precedence(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) log_query(kind="path", question="A -> B", corpus="/g.json", nodes_returned=3) rec = json.loads(log_file.read_text()) assert rec["nodes_returned"] == 3 def test_kind_mcp_query(tmp_path, monkeypatch): log_file = tmp_path / "q.log" monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file)) monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False) log_query(kind="mcp_query", question="q", corpus="/g.json") rec = json.loads(log_file.read_text()) assert rec["kind"] == "mcp_query" # --------------------------------------------------------------------------- # #1797 — query log is opt-in (default OFF) # --------------------------------------------------------------------------- def _clear_log_env(monkeypatch): for k in ("GRAPHIFY_QUERY_LOG", "GRAPHIFY_QUERY_LOG_ENABLE", "GRAPHIFY_QUERY_LOG_DISABLE"): monkeypatch.delenv(k, raising=False) def test_query_log_off_by_default(monkeypatch): from graphify.querylog import _log_path _clear_log_env(monkeypatch) assert _log_path() is None def test_query_log_enabled_by_explicit_flag(monkeypatch): from graphify.querylog import _log_path _clear_log_env(monkeypatch) monkeypatch.setenv("GRAPHIFY_QUERY_LOG_ENABLE", "1") assert str(_log_path()).endswith("graphify-queries.log") def test_query_log_enabled_by_explicit_path(monkeypatch, tmp_path): from graphify.querylog import _log_path _clear_log_env(monkeypatch) monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(tmp_path / "q.log")) assert _log_path() == tmp_path / "q.log" def test_query_log_disable_wins(monkeypatch): from graphify.querylog import _log_path _clear_log_env(monkeypatch) monkeypatch.setenv("GRAPHIFY_QUERY_LOG_ENABLE", "1") monkeypatch.setenv("GRAPHIFY_QUERY_LOG_DISABLE", "1") assert _log_path() is None def test_log_query_writes_nothing_by_default(monkeypatch, tmp_path): """End-to-end: with no opt-in, log_query must not create the default log.""" _clear_log_env(monkeypatch) monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) log_query(kind="query", question="secret internal ticket TICKET-123", corpus=".", result="1 node found") assert not (tmp_path / ".cache" / "graphify-queries.log").exists()