Automated issue triage: categorize, find stale, estimate burn time, generate markdown/JSON reports. Addresses timmy-home backlog (was 220, now 148 open issues). Closes #1459.
124 lines
4.6 KiB
Python
124 lines
4.6 KiB
Python
"""Tests for issue backlog manager."""
|
|
|
|
import json
|
|
from datetime import datetime, timezone, timedelta
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "bin"))
|
|
from issue_backlog_manager import analyze_issue, to_markdown
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_issue():
|
|
return {
|
|
"number": 1234,
|
|
"title": "[BUG] Fix crash on startup",
|
|
"labels": [{"name": "bug"}, {"name": "p1"}],
|
|
"assignees": [{"login": "timmy"}],
|
|
"created_at": "2025-01-01T00:00:00Z",
|
|
"updated_at": "2025-06-01T00:00:00Z",
|
|
"body": "Fixes #999",
|
|
"html_url": "https://forge.example.com/...",
|
|
}
|
|
|
|
|
|
class TestAnalyzeIssue:
|
|
def test_categorizes_bug(self, sample_issue):
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["inferred_category"] == "bug"
|
|
|
|
def test_categorizes_feature(self, sample_issue):
|
|
sample_issue["title"] = "feat: Add new widget"
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["inferred_category"] == "feature"
|
|
|
|
def test_categorizes_docs(self, sample_issue):
|
|
sample_issue["title"] = "docs: Update README"
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["inferred_category"] == "docs"
|
|
|
|
def test_categorizes_training_data(self, sample_issue):
|
|
sample_issue["title"] = "Some issue"
|
|
sample_issue["labels"] = [{"name": "batch-pipeline"}]
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["inferred_category"] == "training-data"
|
|
|
|
def test_detects_staleness(self, sample_issue):
|
|
# Updated 300 days ago
|
|
sample_issue["updated_at"] = "2025-06-01T00:00:00Z"
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["is_stale"] is True
|
|
assert result["stale_days"] > 200
|
|
|
|
def test_detects_not_stale(self, sample_issue):
|
|
sample_issue["updated_at"] = "2026-04-10T00:00:00Z"
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["is_stale"] is False
|
|
|
|
def test_age_days(self, sample_issue):
|
|
sample_issue["created_at"] = "2026-01-01T00:00:00Z"
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["age_days"] > 100
|
|
|
|
def test_has_assignee(self, sample_issue):
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["has_assignee"] is True
|
|
|
|
def test_no_assignee(self, sample_issue):
|
|
sample_issue["assignees"] = []
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["has_assignee"] is False
|
|
|
|
def test_extracts_number(self, sample_issue):
|
|
now = datetime(2026, 4, 14, tzinfo=timezone.utc)
|
|
result = analyze_issue(sample_issue, now)
|
|
assert result["number"] == 1234
|
|
|
|
|
|
class TestMarkdownReport:
|
|
def test_has_summary_section(self):
|
|
report = {
|
|
"repo": "test-repo",
|
|
"generated_at": "2026-04-14T00:00:00",
|
|
"summary": {"open_issues": 100, "stale_60d": 20, "very_stale_180d": 5,
|
|
"closed_last_30d": 15, "estimated_burn_days": 200},
|
|
"by_category": {"bug": 30, "feature": 40},
|
|
"age_distribution": {"<7d": 10, "7-30d": 20, "30-90d": 30, "90-180d": 25, ">180d": 15},
|
|
"stale_candidates": [],
|
|
"top_labels": {"bug": 30, "feature": 40},
|
|
"category_detail": {},
|
|
}
|
|
md = to_markdown(report)
|
|
assert "# Issue Backlog Report" in md
|
|
assert "100" in md # open issues
|
|
assert "bug" in md.lower()
|
|
|
|
def test_shows_stale_candidates(self):
|
|
report = {
|
|
"repo": "test",
|
|
"generated_at": "2026-04-14",
|
|
"summary": {"open_issues": 1, "stale_60d": 1, "very_stale_180d": 1,
|
|
"closed_last_30d": 0, "estimated_burn_days": 999},
|
|
"by_category": {},
|
|
"age_distribution": {},
|
|
"stale_candidates": [{"number": 99, "title": "Old issue", "stale_days": 500}],
|
|
"top_labels": {},
|
|
"category_detail": {},
|
|
}
|
|
md = to_markdown(report)
|
|
assert "#99" in md
|
|
assert "500" in md
|