"""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