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
106 lines
3.6 KiB
Python
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"))
|