mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-13 10:57:13 +00:00
da71f8264f
querylog wrote every query/path/explain question + corpus path (and full responses under GRAPHIFY_QUERY_LOG_RESPONSES) to a default-on, unbounded, fail-silent plaintext file at ~/.cache/graphify-queries.log — outside any repo's .gitignore/retention, and undocumented. A default-on plaintext record of proprietary queries contradicts graphify's on-device / no-telemetry posture. Flip to opt-in: _log_path() returns None unless GRAPHIFY_QUERY_LOG_ENABLE=1 (default path) or GRAPHIFY_QUERY_LOG=<path> is set; GRAPHIFY_QUERY_LOG_DISABLE=1 still forces it off (back-compat, wins). Document all four env vars in the README (the old entries implied default-on). Regression tests cover default-off, both enable paths, disable-wins, and that log_query writes nothing without opt-in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
224 lines
7.8 KiB
Python
224 lines
7.8 KiB
Python
"""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()
|