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
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""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"))
|
||||
Reference in New Issue
Block a user