162 lines
4.9 KiB
Python
162 lines
4.9 KiB
Python
"""Tests for PR triage automation (#659)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from datetime import datetime, timezone, timedelta
|
|
from scripts.pr_triage import categorize, refs, find_duplicates, health, is_safe_to_merge
|
|
|
|
|
|
class TestCategorize:
|
|
"""PR categorization from title/body/labels."""
|
|
|
|
def test_training_data(self):
|
|
pr = {"title": "Add DPO training data", "body": "", "labels": []}
|
|
assert categorize(pr) == "training-data"
|
|
|
|
def test_bug_fix(self):
|
|
pr = {"title": "fix: resolve crash on startup", "body": "", "labels": []}
|
|
assert categorize(pr) == "bug-fix"
|
|
|
|
def test_feature(self):
|
|
pr = {"title": "feat: add dark mode", "body": "", "labels": []}
|
|
assert categorize(pr) == "feature"
|
|
|
|
def test_maintenance(self):
|
|
pr = {"title": "refactor: simplify auth flow", "body": "", "labels": []}
|
|
assert categorize(pr) == "maintenance"
|
|
|
|
def test_other(self):
|
|
pr = {"title": "Update readme", "body": "", "labels": []}
|
|
assert categorize(pr) == "other"
|
|
|
|
|
|
class TestRefs:
|
|
"""Issue reference extraction."""
|
|
|
|
def test_extracts_from_title(self):
|
|
pr = {"title": "fix: resolve #123", "body": ""}
|
|
assert refs(pr) == [123]
|
|
|
|
def test_extracts_from_body(self):
|
|
pr = {"title": "Fix", "body": "Closes #456, refs #789"}
|
|
assert refs(pr) == [456, 789]
|
|
|
|
def test_no_refs(self):
|
|
pr = {"title": "Fix", "body": "No issue refs"}
|
|
assert refs(pr) == []
|
|
|
|
def test_multiple_refs(self):
|
|
pr = {"title": "#1 and #2", "body": "Also #3"}
|
|
assert refs(pr) == [1, 2, 3]
|
|
|
|
|
|
class TestFindDuplicates:
|
|
"""Duplicate PR detection."""
|
|
|
|
def test_ref_based_duplicates(self):
|
|
prs = [
|
|
{"number": 1, "title": "Fix #100", "body": "Closes #100"},
|
|
{"number": 2, "title": "Fix #100 too", "body": "Closes #100"},
|
|
]
|
|
dups = find_duplicates(prs)
|
|
assert len(dups) == 1
|
|
assert dups[0]["type"] == "ref"
|
|
|
|
def test_title_similarity_duplicates(self):
|
|
prs = [
|
|
{"number": 1, "title": "feat: add dark mode support", "body": ""},
|
|
{"number": 2, "title": "feat: add dark mode support", "body": "different body"},
|
|
]
|
|
dups = find_duplicates(prs)
|
|
assert len(dups) >= 1
|
|
assert any(d["type"] == "similarity" for d in dups)
|
|
|
|
def test_no_duplicates(self):
|
|
prs = [
|
|
{"number": 1, "title": "Fix auth bug", "body": "Closes #100"},
|
|
{"number": 2, "title": "Add dark mode", "body": "Closes #200"},
|
|
]
|
|
dups = find_duplicates(prs)
|
|
assert len(dups) == 0
|
|
|
|
|
|
class TestHealth:
|
|
"""PR health assessment."""
|
|
|
|
def _make_pr(self, **overrides):
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
pr = {
|
|
"number": 1,
|
|
"title": "test",
|
|
"body": "Closes #100",
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
"head": {"ref": "fix/test"},
|
|
"mergeable": True,
|
|
"user": {"login": "agent"},
|
|
"labels": [],
|
|
}
|
|
pr.update(overrides)
|
|
return pr
|
|
|
|
def test_basic_health(self):
|
|
pr = self._make_pr()
|
|
h = health(pr, {100: {"number": 100}})
|
|
assert h["pr"] == 1
|
|
assert h["refs"] == [100]
|
|
assert h["open_issues"] == [100]
|
|
assert h["age_days"] == 0
|
|
|
|
def test_stale_detection(self):
|
|
old = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
|
|
pr = self._make_pr(created_at=old, updated_at=old)
|
|
h = health(pr, {})
|
|
assert h["stale_days"] >= 29
|
|
assert h["risk_score"] > 30
|
|
|
|
|
|
class TestIsSafeToMerge:
|
|
"""Auto-merge safety checks."""
|
|
|
|
def _make_health(self, **overrides):
|
|
h = {
|
|
"pr": 1, "title": "test", "head": "fix/test",
|
|
"category": "training-data", "refs": [100],
|
|
"open_issues": [100], "closed_issues": [],
|
|
"age_days": 1, "stale_days": 1,
|
|
"risk_score": 10, "mergeable": True,
|
|
"author": "agent", "labels": [],
|
|
}
|
|
h.update(overrides)
|
|
return h
|
|
|
|
def test_safe_training_data(self):
|
|
h = self._make_health()
|
|
ok, reason = is_safe_to_merge(h)
|
|
assert ok
|
|
|
|
def test_unsafe_not_training(self):
|
|
h = self._make_health(category="bug-fix")
|
|
ok, reason = is_safe_to_merge(h)
|
|
assert not ok
|
|
assert "not training-data" in reason
|
|
|
|
def test_unsafe_conflicts(self):
|
|
h = self._make_health(mergeable=False)
|
|
ok, reason = is_safe_to_merge(h)
|
|
assert not ok
|
|
assert "conflicts" in reason
|
|
|
|
def test_unsafe_too_stale(self):
|
|
h = self._make_health(stale_days=31)
|
|
ok, reason = is_safe_to_merge(h)
|
|
assert not ok
|
|
assert "stale" in reason
|
|
|
|
def test_unsafe_high_risk(self):
|
|
h = self._make_health(risk_score=60)
|
|
ok, reason = is_safe_to_merge(h)
|
|
assert not ok
|
|
assert "risk" in reason
|