Files
2026-07-13 12:42:18 +08:00

929 lines
31 KiB
Python

"""Tests for the evaluation framework (scorer, reporter, runner, benchmarks)."""
import csv
import os
import subprocess
import tempfile
from pathlib import Path
import pytest
from code_review_graph.eval.reporter import (
generate_full_report,
generate_markdown_report,
generate_readme_tables,
)
try:
import yaml as _yaml # noqa: F401
from code_review_graph.eval.runner import write_csv
_HAS_YAML = True
except ImportError:
_HAS_YAML = False
write_csv = None # type: ignore[assignment]
from code_review_graph.eval.scorer import (
compute_mrr,
compute_precision_recall,
compute_token_efficiency,
)
# --- Existing scorer tests ---
def test_token_efficiency():
result = compute_token_efficiency(10000, 3000)
assert result["raw_tokens"] == 10000
assert result["graph_tokens"] == 3000
assert result["ratio"] == 0.3
assert result["reduction_percent"] == 70.0
def test_token_efficiency_zero_raw():
result = compute_token_efficiency(0, 100)
assert result["ratio"] == 0.0
assert result["reduction_percent"] == 0.0
def test_mrr_found_at_rank_2():
result = compute_mrr("b", ["a", "b", "c"])
assert result == 0.5
def test_mrr_found_at_rank_1():
result = compute_mrr("a", ["a", "b", "c"])
assert result == 1.0
def test_mrr_not_found():
result = compute_mrr("z", ["a", "b", "c"])
assert result == 0.0
def test_precision_recall():
predicted = {"a", "b", "c", "d"}
actual = {"b", "c", "e"}
result = compute_precision_recall(predicted, actual)
assert result["precision"] == 0.5
assert result["recall"] == round(2 / 3, 4)
expected_f1 = round(2 * 0.5 * (2 / 3) / (0.5 + 2 / 3), 4)
assert result["f1"] == expected_f1
def test_precision_recall_empty_sets():
result = compute_precision_recall(set(), set())
assert result["precision"] == 1.0
assert result["recall"] == 1.0
assert result["f1"] == 1.0
def test_precision_recall_no_overlap():
result = compute_precision_recall({"a"}, {"b"})
assert result["precision"] == 0.0
assert result["recall"] == 0.0
assert result["f1"] == 0.0
def test_generate_markdown_report():
results = [
{
"benchmark": "token_efficiency",
"ratio": 0.3,
"reduction_percent": 70.0,
},
{
"benchmark": "search_mrr",
"ratio": "-",
"reduction_percent": "-",
},
]
report = generate_markdown_report(results)
assert "# Evaluation Report" in report
assert "## Summary" in report
assert "token_efficiency" in report
assert "search_mrr" in report
assert "70.0" in report
assert "| Benchmark |" in report
def test_generate_markdown_report_empty():
report = generate_markdown_report([])
assert "No benchmark results" in report
# --- New tests ---
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
def test_load_config():
"""Load a temp YAML config and verify structure."""
import yaml
with tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False
) as f:
yaml.dump(
{
"name": "test-repo",
"url": "https://example.com/repo.git",
"commit": "HEAD",
"language": "python",
"size_category": "small",
"test_commits": [{"sha": "abc123", "description": "test"}],
"entry_points": ["main.py::main"],
"search_queries": [
{"query": "hello", "expected": "main.py::greet"}
],
},
f,
)
tmp_path = f.name
try:
import yaml as _yaml
with open(tmp_path) as fh:
config = _yaml.safe_load(fh)
assert config["name"] == "test-repo"
assert config["language"] == "python"
assert len(config["test_commits"]) == 1
assert len(config["entry_points"]) == 1
assert len(config["search_queries"]) == 1
finally:
os.unlink(tmp_path)
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
def test_write_csv():
"""Write results to CSV and read back."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "results" / "test.csv"
results = [
{"repo": "foo", "tokens": 100, "ratio": 2.5},
{"repo": "bar", "tokens": 200, "ratio": 1.5},
]
write_csv(results, path)
assert path.exists()
with open(path, newline="") as f:
reader = csv.DictReader(f)
rows = list(reader)
assert len(rows) == 2
assert rows[0]["repo"] == "foo"
assert rows[1]["tokens"] == "200"
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
def test_write_csv_empty():
"""Writing empty results should be a no-op."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "empty.csv"
write_csv([], path)
assert not path.exists()
def test_generate_readme_tables():
"""Feed sample CSV data and verify table format."""
with tempfile.TemporaryDirectory() as tmpdir:
results_dir = Path(tmpdir)
# Write token efficiency CSV
te_path = results_dir / "test_token_efficiency_2026-01-01.csv"
with open(te_path, "w", newline="") as f:
w = csv.DictWriter(
f,
fieldnames=[
"repo", "commit", "description", "changed_files",
"naive_tokens", "standard_tokens", "graph_tokens",
"naive_to_graph_ratio", "standard_to_graph_ratio",
],
)
w.writeheader()
w.writerow({
"repo": "myrepo", "commit": "abc", "description": "test",
"changed_files": "3", "naive_tokens": "1000",
"standard_tokens": "500", "graph_tokens": "200",
"naive_to_graph_ratio": "5.0",
"standard_to_graph_ratio": "2.5",
})
tables = generate_readme_tables(results_dir)
assert "### Token Efficiency" in tables
assert "myrepo" in tables
assert "1000" in tables
def test_generate_full_report():
"""Feed sample CSV data and verify report sections."""
with tempfile.TemporaryDirectory() as tmpdir:
results_dir = Path(tmpdir)
# Write a build_performance CSV
bp_path = results_dir / "test_build_performance_2026-01-01.csv"
with open(bp_path, "w", newline="") as f:
w = csv.DictWriter(
f,
fieldnames=[
"repo", "file_count", "node_count", "edge_count",
"flow_detection_seconds", "community_detection_seconds",
"search_avg_ms", "nodes_per_second",
],
)
w.writeheader()
w.writerow({
"repo": "testrepo", "file_count": "10", "node_count": "50",
"edge_count": "30", "flow_detection_seconds": "0.1",
"community_detection_seconds": "0.2",
"search_avg_ms": "5.0", "nodes_per_second": "500",
})
report = generate_full_report(results_dir)
assert "# Evaluation Report" in report
assert "## Methodology" in report
assert "## Build Performance" in report
assert "testrepo" in report
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
def test_runner_with_mock_repo():
"""Create a tiny git repo with 2 Python files, run benchmarks, verify output."""
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir) / "mock_repo"
repo_path.mkdir()
# Init git repo
subprocess.run(
["git", "init"], cwd=str(repo_path), capture_output=True
)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=str(repo_path), capture_output=True,
)
# Create two Python files
(repo_path / "main.py").write_text(
'from helper import greet\n\ndef main():\n greet("world")\n',
encoding="utf-8",
)
(repo_path / "helper.py").write_text(
'def greet(name):\n print(f"Hello {name}")\n',
encoding="utf-8",
)
subprocess.run(
["git", "add", "."], cwd=str(repo_path), capture_output=True
)
subprocess.run(
["git", "commit", "-m", "initial"],
cwd=str(repo_path), capture_output=True,
)
# Second commit: modify helper.py
(repo_path / "helper.py").write_text(
'def greet(name):\n print(f"Hi {name}!")\n',
encoding="utf-8",
)
subprocess.run(
["git", "add", "."], cwd=str(repo_path), capture_output=True
)
subprocess.run(
["git", "commit", "-m", "update greeting"],
cwd=str(repo_path), capture_output=True,
)
# Build graph
from code_review_graph.graph import GraphStore
from code_review_graph.incremental import full_build, get_db_path
db_path = get_db_path(repo_path)
store = GraphStore(db_path)
full_build(repo_path, store)
config = {
"name": "mock",
"language": "python",
"test_commits": [
{"sha": "HEAD", "description": "update greeting"},
],
"entry_points": ["main.py::main"],
"search_queries": [
{"query": "greet", "expected": "helper.py::greet"},
],
}
# Run token_efficiency
from code_review_graph.eval.benchmarks import token_efficiency
te_results = token_efficiency.run(repo_path, store, config)
assert len(te_results) >= 1
assert "naive_tokens" in te_results[0]
assert "graph_tokens" in te_results[0]
# Run impact_accuracy
from code_review_graph.eval.benchmarks import impact_accuracy
ia_results = impact_accuracy.run(repo_path, store, config)
assert len(ia_results) >= 1
assert "precision" in ia_results[0]
assert "f1" in ia_results[0]
# Run search_quality
from code_review_graph.eval.benchmarks import search_quality
sq_results = search_quality.run(repo_path, store, config)
assert len(sq_results) == 1
assert "reciprocal_rank" in sq_results[0]
# Run build_performance
from code_review_graph.eval.benchmarks import build_performance
bp_results = build_performance.run(repo_path, store, config)
assert len(bp_results) == 1
assert "node_count" in bp_results[0]
assert bp_results[0]["node_count"] > 0
store.close()
# --- Token benchmark tests ---
def test_estimate_tokens_basic():
"""estimate_tokens should return a reasonable approximation."""
from code_review_graph.eval.token_benchmark import estimate_tokens
# Simple string: "hello" => JSON '"hello"' (7 chars) => 7 // 4 = 1
assert estimate_tokens("hello") == 1
# Dict: {"a": 1} => '{"a": 1}' (8 chars) => 8 // 4 = 2
assert estimate_tokens({"a": 1}) == 2
# Longer content should scale proportionally
long_text = "x" * 400
tokens = estimate_tokens(long_text)
# JSON adds 2 quote chars: (400 + 2) // 4 = 100
assert tokens == 100
def test_estimate_tokens_nested():
"""estimate_tokens handles nested structures."""
from code_review_graph.eval.token_benchmark import estimate_tokens
nested = {"nodes": [{"name": "foo"}, {"name": "bar"}], "count": 2}
tokens = estimate_tokens(nested)
assert tokens > 0
assert isinstance(tokens, int)
def test_estimate_tokens_non_serializable():
"""estimate_tokens uses default=str for non-serializable objects."""
from pathlib import Path
from code_review_graph.eval.token_benchmark import estimate_tokens
# Path objects are not JSON-serializable but default=str handles them
tokens = estimate_tokens({"path": Path("/tmp/test")})
assert tokens > 0
def test_benchmark_review_workflow():
"""benchmark_review_workflow completes and returns expected structure."""
from code_review_graph.eval.token_benchmark import benchmark_review_workflow
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir) / "bench_repo"
repo_path.mkdir()
# Init git repo with two commits
subprocess.run(
["git", "init"], cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=str(repo_path), capture_output=True,
)
(repo_path / "main.py").write_text(
'from helper import greet\n\ndef main():\n greet("world")\n',
encoding="utf-8",
)
(repo_path / "helper.py").write_text(
'def greet(name):\n print(f"Hello {name}")\n',
encoding="utf-8",
)
subprocess.run(
["git", "add", "."], cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "commit", "-m", "initial"],
cwd=str(repo_path), capture_output=True,
)
# Second commit
(repo_path / "helper.py").write_text(
'def greet(name):\n print(f"Hi {name}!")\n',
encoding="utf-8",
)
subprocess.run(
["git", "add", "."], cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "commit", "-m", "update greeting"],
cwd=str(repo_path), capture_output=True,
)
# Build graph
from code_review_graph.graph import GraphStore
from code_review_graph.incremental import full_build, get_db_path
db_path = get_db_path(repo_path)
store = GraphStore(db_path)
full_build(repo_path, store)
store.close()
# Run the review benchmark
result = benchmark_review_workflow(
repo_root=str(repo_path), base="HEAD~1",
)
assert result["workflow"] == "review"
assert result["total_tokens"] > 0
assert result["tool_calls"] == 2
assert len(result["calls"]) == 2
assert result["calls"][0]["tool"] == "get_minimal_context"
assert result["calls"][1]["tool"] == "detect_changes_minimal"
for call in result["calls"]:
assert call["tokens"] >= 0
def test_run_all_benchmarks():
"""run_all_benchmarks returns results for all workflows."""
from code_review_graph.eval.token_benchmark import run_all_benchmarks
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = Path(tmpdir) / "all_bench_repo"
repo_path.mkdir()
subprocess.run(
["git", "init"], cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=str(repo_path), capture_output=True,
)
(repo_path / "app.py").write_text(
'def main():\n print("hello")\n',
encoding="utf-8",
)
subprocess.run(
["git", "add", "."], cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "commit", "-m", "initial"],
cwd=str(repo_path), capture_output=True,
)
(repo_path / "app.py").write_text(
'def main():\n print("hi")\n',
encoding="utf-8",
)
subprocess.run(
["git", "add", "."], cwd=str(repo_path), capture_output=True,
)
subprocess.run(
["git", "commit", "-m", "update"],
cwd=str(repo_path), capture_output=True,
)
from code_review_graph.graph import GraphStore
from code_review_graph.incremental import full_build, get_db_path
db_path = get_db_path(repo_path)
store = GraphStore(db_path)
full_build(repo_path, store)
store.close()
results = run_all_benchmarks(repo_root=str(repo_path), base="HEAD~1")
# Should have one result per workflow (5 total)
assert len(results) == 5
workflow_names = {r["workflow"] for r in results}
assert workflow_names == {
"review", "architecture", "debug", "onboard", "pre_merge",
}
# Each successful result should have total_tokens
for r in results:
if "error" not in r:
assert r["total_tokens"] >= 0
assert "calls" in r
# --- Failure-inflation regression tests + agent_baseline + co-change mode ---
def _git(repo_path, *args):
subprocess.run(["git", *args], cwd=str(repo_path), capture_output=True)
def _make_repo(tmpdir, two_file_commit=False):
"""Tiny git repo: initial commit, then a second commit touching 1 or 2 files."""
repo_path = Path(tmpdir) / "mock_repo"
repo_path.mkdir()
_git(repo_path, "init")
_git(repo_path, "config", "user.email", "test@test.com")
_git(repo_path, "config", "user.name", "Test")
(repo_path / "main.py").write_text(
'from helper import greet\n\ndef main():\n greet("world")\n',
encoding="utf-8",
)
(repo_path / "helper.py").write_text(
'def greet(name):\n print(f"Hello {name}")\n',
encoding="utf-8",
)
_git(repo_path, "add", ".")
_git(repo_path, "commit", "-m", "initial")
(repo_path / "helper.py").write_text(
'def greet(name):\n print(f"Hi {name}!")\n',
encoding="utf-8",
)
if two_file_commit:
(repo_path / "main.py").write_text(
'from helper import greet\n\ndef main():\n greet("there")\n',
encoding="utf-8",
)
_git(repo_path, "add", ".")
_git(repo_path, "commit", "-m", "update greeting")
return repo_path
def _build_store(repo_path):
from code_review_graph.graph import GraphStore
from code_review_graph.incremental import full_build, get_db_path
store = GraphStore(get_db_path(repo_path))
full_build(repo_path, store)
return store
def _mock_config(**extra):
config = {
"name": "mock",
"language": "python",
"test_commits": [{"sha": "HEAD", "description": "update greeting"}],
"entry_points": ["main.py::main"],
"search_queries": [{"query": "greet", "expected": "helper.py::greet"}],
}
config.update(extra)
return config
def test_token_efficiency_failure_marked_error_not_inflated(monkeypatch):
"""A thrown get_review_context must yield status=error, not ratio=naive/1."""
from code_review_graph.eval.benchmarks import token_efficiency
def _boom(**kwargs):
raise RuntimeError("boom")
monkeypatch.setattr("code_review_graph.tools.get_review_context", _boom)
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir)
store = _build_store(repo_path)
try:
results = token_efficiency.run(repo_path, store, _mock_config())
finally:
store.close()
assert len(results) >= 1
for row in results:
assert row["status"] == "error"
assert "boom" in row["error"]
# Failed measurements must not look like valid (inflated) ratios.
assert row["graph_tokens"] == ""
assert row["naive_to_graph_ratio"] == ""
assert row["standard_to_graph_ratio"] == ""
agg = token_efficiency.aggregate(results)
assert agg["ok_rows"] == 0
assert agg["error_rows"] == len(results)
assert agg["median_naive_to_graph_ratio"] is None
def test_token_efficiency_success_rows_status_ok():
from code_review_graph.eval.benchmarks import token_efficiency
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir)
store = _build_store(repo_path)
try:
results = token_efficiency.run(repo_path, store, _mock_config())
finally:
store.close()
assert len(results) >= 1
for row in results:
assert row["status"] == "ok"
assert row["error"] == ""
assert isinstance(row["graph_tokens"], int)
assert isinstance(row["naive_to_graph_ratio"], float)
agg = token_efficiency.aggregate(results)
assert agg["ok_rows"] == len(results)
assert agg["error_rows"] == 0
assert isinstance(agg["median_naive_to_graph_ratio"], float)
def test_impact_accuracy_failure_marked_error_not_perfect_recall(monkeypatch):
"""A thrown analyze_changes must not silently score recall 1.0."""
from code_review_graph.eval.benchmarks import impact_accuracy
def _boom(*args, **kwargs):
raise RuntimeError("analysis exploded")
monkeypatch.setattr("code_review_graph.changes.analyze_changes", _boom)
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir, two_file_commit=True)
store = _build_store(repo_path)
try:
results = impact_accuracy.run(repo_path, store, _mock_config())
finally:
store.close()
assert len(results) >= 2 # both modes attempted, both failed
for row in results:
assert row["status"] == "error"
assert "analysis exploded" in row["error"]
assert row["recall"] == "" # NOT 1.0
assert row["precision"] == ""
assert row["f1"] == ""
agg = impact_accuracy.aggregate(results)
assert agg["graph_derived"]["ok_rows"] == 0
assert agg["co_change"]["ok_rows"] == 0
assert agg["graph_derived"]["mean_recall"] is None
assert agg["error_rows"] == len(results)
def test_impact_accuracy_emits_both_ground_truth_modes():
from code_review_graph.eval.benchmarks import impact_accuracy
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir, two_file_commit=True)
store = _build_store(repo_path)
try:
results = impact_accuracy.run(repo_path, store, _mock_config())
finally:
store.close()
modes = {r["ground_truth_mode"] for r in results}
assert impact_accuracy.MODE_GRAPH_DERIVED in modes
assert impact_accuracy.MODE_CO_CHANGE in modes
graph_rows = [
r for r in results
if r["ground_truth_mode"] == impact_accuracy.MODE_GRAPH_DERIVED
]
co_rows = [
r for r in results
if r["ground_truth_mode"] == impact_accuracy.MODE_CO_CHANGE
]
for row in graph_rows:
assert row["status"] == "ok"
assert 0.0 <= row["recall"] <= 1.0
assert row["seed_file"] == ""
# Commit touched helper.py + main.py: seed is the sorted-first file and
# the ground truth is the *other* co-changed file — independent of the graph.
assert len(co_rows) == 1
co = co_rows[0]
assert co["status"] == "ok"
assert co["seed_file"] == "helper.py"
assert co["actual_files"] == 1
assert 0.0 <= co["precision"] <= 1.0
assert 0.0 <= co["recall"] <= 1.0
def test_impact_accuracy_co_change_skipped_for_single_file_commit():
from code_review_graph.eval.benchmarks import impact_accuracy
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir, two_file_commit=False)
store = _build_store(repo_path)
try:
results = impact_accuracy.run(repo_path, store, _mock_config())
finally:
store.close()
co_rows = [
r for r in results
if r["ground_truth_mode"] == impact_accuracy.MODE_CO_CHANGE
]
assert len(co_rows) == 1
assert co_rows[0]["status"] == "skipped"
assert "co-changed" in co_rows[0]["error"]
agg = impact_accuracy.aggregate(results)
assert agg["skipped_rows"] == 1
assert agg["co_change"]["ok_rows"] == 0
# --- agent_baseline benchmark ---
def test_derive_search_terms_extracts_identifiers_and_keywords():
from code_review_graph.eval.benchmarks.agent_baseline import derive_search_terms
terms = derive_search_terms("How does Client.request send an HTTP request?")
assert "client.request" in terms
assert "how" not in terms # stopword
assert "does" not in terms # stopword
assert all(t == t.lower() for t in terms)
def test_grep_rank_orders_by_match_count_and_takes_top_k():
from code_review_graph.eval.benchmarks.agent_baseline import grep_rank
with tempfile.TemporaryDirectory() as tmpdir:
corpus = Path(tmpdir)
(corpus / "a.py").write_text("greet()\ngreet()\ngreet()\n", encoding="utf-8")
(corpus / "b.py").write_text("greet()\n", encoding="utf-8")
(corpus / "c.py").write_text("nothing here\n", encoding="utf-8")
(corpus / "d.txt").write_text("greet greet greet greet\n", encoding="utf-8")
sub = corpus / "node_modules"
sub.mkdir()
(sub / "e.py").write_text("greet greet greet greet greet\n", encoding="utf-8")
ranked = grep_rank(corpus, ["greet"], k=3)
# d.txt (non-source ext) and node_modules/e.py (skipped dir) excluded
assert ranked == [("a.py", 3), ("b.py", 1)]
top1 = grep_rank(corpus, ["greet"], k=1)
assert top1 == [("a.py", 3)]
assert grep_rank(corpus, [], k=3) == []
def test_grep_rank_tie_breaks_on_path():
from code_review_graph.eval.benchmarks.agent_baseline import grep_rank
with tempfile.TemporaryDirectory() as tmpdir:
corpus = Path(tmpdir)
(corpus / "zz.py").write_text("token token\n", encoding="utf-8")
(corpus / "aa.py").write_text("token token\n", encoding="utf-8")
ranked = grep_rank(corpus, ["token"], k=2)
assert ranked == [("aa.py", 2), ("zz.py", 2)]
def test_agent_baseline_run_with_mock_repo():
from code_review_graph.eval.benchmarks import agent_baseline
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir)
store = _build_store(repo_path)
config = _mock_config(
agent_questions=["How does greet print a greeting"],
)
try:
results = agent_baseline.run(repo_path, store, config)
finally:
store.close()
assert len(results) == 1
row = results[0]
assert row["question"] == "How does greet print a greeting"
assert "greet" in row["terms"]
assert row["files_matched"] >= 1
assert "helper.py" in row["top_files"]
assert row["baseline_tokens"] > 0
assert row["status"] in ("ok", "no_graph_results")
if row["status"] == "ok":
assert isinstance(row["baseline_to_graph_ratio"], float)
def test_agent_baseline_falls_back_to_search_queries():
from code_review_graph.eval.benchmarks import agent_baseline
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir)
store = _build_store(repo_path)
try:
results = agent_baseline.run(repo_path, store, _mock_config())
finally:
store.close()
assert len(results) == 1
assert results[0]["question"] == "greet"
def test_agent_baseline_search_failure_marked_error(monkeypatch):
from code_review_graph.eval.benchmarks import agent_baseline
def _boom(*args, **kwargs):
raise RuntimeError("search down")
monkeypatch.setattr("code_review_graph.search.hybrid_search", _boom)
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = _make_repo(tmpdir)
store = _build_store(repo_path)
config = _mock_config(agent_questions=["How does greet work"])
try:
results = agent_baseline.run(repo_path, store, config)
finally:
store.close()
assert len(results) == 1
assert results[0]["status"] == "error"
assert "search down" in results[0]["error"]
assert results[0]["baseline_to_graph_ratio"] == ""
agg = agent_baseline.aggregate(results)
assert agg["ok_rows"] == 0
assert agg["error_rows"] == 1
assert agg["median_baseline_to_graph_ratio"] is None
def test_agent_baseline_aggregate_excludes_non_ok_rows():
from code_review_graph.eval.benchmarks import agent_baseline
rows = [
{"status": "ok", "baseline_to_graph_ratio": 4.0},
{"status": "ok", "baseline_to_graph_ratio": 8.0},
{"status": "error", "baseline_to_graph_ratio": ""},
{"status": "no_graph_results", "baseline_to_graph_ratio": ""},
]
agg = agent_baseline.aggregate(rows)
assert agg["total_rows"] == 4
assert agg["ok_rows"] == 2
assert agg["error_rows"] == 1
assert agg["median_baseline_to_graph_ratio"] == 6.0
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
def test_agent_baseline_registered_in_runner():
from code_review_graph.eval.runner import BENCHMARK_REGISTRY
assert "agent_baseline" in BENCHMARK_REGISTRY
def test_reporter_impact_f1_skips_error_and_co_change_rows():
"""Table B must aggregate only ok graph-derived rows."""
with tempfile.TemporaryDirectory() as tmpdir:
results_dir = Path(tmpdir)
ia_path = results_dir / "mock_impact_accuracy_2026-01-01.csv"
fieldnames = [
"repo", "commit", "ground_truth_mode", "seed_file",
"predicted_files", "actual_files", "true_positives",
"precision", "recall", "f1", "status", "error",
]
with open(ia_path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=fieldnames)
w.writeheader()
w.writerow({
"repo": "mock", "commit": "abc",
"ground_truth_mode": "graph-derived (circular — upper bound)",
"seed_file": "", "predicted_files": "2", "actual_files": "2",
"true_positives": "1", "precision": "0.5", "recall": "0.5",
"f1": "0.5", "status": "ok", "error": "",
})
w.writerow({
"repo": "mock", "commit": "def",
"ground_truth_mode": "graph-derived (circular — upper bound)",
"seed_file": "", "predicted_files": "", "actual_files": "",
"true_positives": "", "precision": "", "recall": "",
"f1": "", "status": "error", "error": "boom",
})
w.writerow({
"repo": "mock", "commit": "abc",
"ground_truth_mode": "co-change (same commit, seed excluded)",
"seed_file": "a.py", "predicted_files": "1", "actual_files": "1",
"true_positives": "1", "precision": "1.0", "recall": "1.0",
"f1": "0.9", "status": "ok", "error": "",
})
tables = generate_readme_tables(results_dir)
# 0.5 comes only from the single ok graph-derived row; the error row and
# the co-change row (different metric) must not pollute the column.
assert "0.5" in tables
assert "0.9" not in tables