Files
code-review-graph/tests/test_report.py
T
dev 84ae9b817e feat: add unified-review workflow (scoring tools + skill)
Adds the unified-review integration that fuses CRG graph context with the
ai-code-review scoring methodology and gstack-review fix-first workflow:

- scoring.py: objective Layer-2 metrics (sql_risk, exception_coverage,
  redundancy_rate, high_risk_density, vulnerability_risk) with
  good/warn/fail grades, plus dedupe_findings (fingerprint merge,
  multi-source confidence boost, PR quality score) and report data builder
- tools/scoring_tools.py + main.py: three new MCP tools
  (score_review_tool, dedupe_findings_tool, generate_report_tool)
- assets/report-template.html: self-contained HTML report template
- skills.py + skills/unified-review/: new read-only unified-review skill
  with language/manual-review/specialist checklists
- docs and CHANGELOG updated; tests added (test_scoring, test_report,
  test_unified_review) and test_skills updated for 5 skills
2026-08-05 13:31:55 +08:00

106 lines
3.6 KiB
Python

"""Tests for the unified-review HTML report tool."""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from code_review_graph.scoring import build_report_data # noqa: E402
from code_review_graph.tools.scoring_tools import ( # noqa: E402
_load_report_template,
generate_report_func,
)
def _review_data() -> dict:
return {
"scope": "change-level",
"tier": "standard",
"timestamp": "2026-08-05T00:00:00Z",
"files": "app.py, main.py",
"baseline": "generic",
"verdict": "\u2705 PASS",
"metrics": {
"sql_risk": {"value": 0, "grade": "good", "note": "heuristic"},
"exception_coverage": {"value": 33.33, "grade": "warn", "note": "heuristic"},
},
"findings": [
{
"severity": "MAJOR",
"category": "Data",
"message": "Possible N+1 query",
"path": "src/app.py",
"line": 42,
"confidence": 7,
"fix": "Add eager loading",
},
],
"manual_review": ["Payment callback idempotency"],
"llm_judged": ["requirement_coverage", "logic_alignment"],
}
class TestBuildReportData:
def test_normalises_review_data(self):
data = build_report_data(_review_data())
assert data["verdict"] == "\u2705 PASS"
assert data["tier"] == "standard"
assert data["scope"] == "change-level"
assert data["metrics"]["sql_risk"]["grade"] == "good"
assert len(data["issues"]) == 1
assert data["issues"][0]["location"] == "src/app.py:42"
assert "requirement_coverage" in data["llm_judged"]
def test_empty_findings(self):
rd = _review_data()
rd["findings"] = []
data = build_report_data(rd)
assert data["issues"] == []
def test_defaults(self):
data = build_report_data({})
assert data["scope"] == "change-level"
assert data["tier"] == "standard"
assert data["verdict"] == "\u274c FAIL"
assert data["issues"] == []
class TestLoadTemplate:
def test_loads_package_asset(self):
template = _load_report_template()
assert "{{REPORT_DATA}}" in template
assert template.startswith("<!DOCTYPE html>")
class TestGenerateReport:
def test_writes_self_contained_html(self, tmp_path):
Path(tmp_path, ".code-review-graph").mkdir(exist_ok=True)
out = tmp_path / "sub" / "report.html"
result = generate_report_func(
_review_data(),
output_path=str(out),
repo_root=str(tmp_path),
)
assert result["status"] == "ok"
assert Path(result["output_path"]).is_file()
html = Path(result["output_path"]).read_text(encoding="utf-8")
assert "{{REPORT_DATA}}" not in html
assert "\u2705 PASS" in html
assert "N+1" in html
def test_default_output_path_is_repo_root(self, tmp_path):
# repo_root must look like a project root: create .code-review-graph.
Path(tmp_path, ".code-review-graph").mkdir(exist_ok=True)
result = generate_report_func(_review_data(), repo_root=str(tmp_path))
assert result["status"] == "ok"
assert result["output_path"].endswith("code-review-report.html")
assert Path(result["output_path"]).is_file()
def test_handles_missing_repo_root(self, tmp_path):
with pytest.raises(ValueError):
generate_report_func(_review_data(), repo_root=str(tmp_path / "missing"))