[AUDIT][ACTION] Add issue backlog triage tool — enabler for #478
Implements scripts/issue_backlog_triage.py — automated issue backlog analysis and triage for Gitea repos, addressing the 559-issue backlog audit finding. Features: - Paginated fetch of all open issues across repos - Keyword-based categorization (adversary, bug, security, training_data, …) - Duplicate detection via issue reference (#N) sharing - Stale identification (>14d with no activity) - Optional dry-run close of stale issues (--close-stale) - Optional priority label application (P0–P3) with auto-creation (--apply-priority) - Markdown and JSON report outputs Unit tests added in tests/test_issue_backlog_triage.py (27 tests, all passing). Enables systematic sweep of timmy-home, timmy-config, the-nexus, and hermes-agent backlogs per issue #478 acceptance criteria. Closes #478
This commit is contained in:
154
tests/test_issue_backlog_triage.py
Normal file
154
tests/test_issue_backlog_triage.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for issue_backlog_triage.py — Issue #478."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from issue_backlog_triage import (
|
||||
categorize_issue,
|
||||
extract_refs,
|
||||
find_duplicates,
|
||||
is_stale,
|
||||
STALE_DAYS,
|
||||
)
|
||||
|
||||
|
||||
class TestCategorize:
|
||||
def test_training_data(self):
|
||||
issue = {"title": "feat: 500 emotional weather pairs (#603)"}
|
||||
assert categorize_issue(issue) == "training_data"
|
||||
|
||||
def test_scene_description(self):
|
||||
issue = {"title": "Scene Descriptions: Jazz — 100 Lyrics→Visual"}
|
||||
assert categorize_issue(issue) == "training_data"
|
||||
|
||||
def test_adversary(self):
|
||||
issue = {"title": "Adversary: Jailbreak Generator — 1K Prompts"}
|
||||
assert categorize_issue(issue) == "adversary"
|
||||
|
||||
def test_bug(self):
|
||||
issue = {"title": "fix: broken import in cli.py"}
|
||||
assert categorize_issue(issue) == "bug"
|
||||
|
||||
def test_feature(self):
|
||||
issue = {"title": "feat: add token budget tracker"}
|
||||
assert categorize_issue(issue) == "feature"
|
||||
|
||||
def test_docs(self):
|
||||
issue = {"title": "docs: update README with new config format"}
|
||||
assert categorize_issue(issue) == "docs"
|
||||
|
||||
def test_ops(self):
|
||||
issue = {"title": "ops: deploy config to VPS"}
|
||||
assert categorize_issue(issue) == "ops"
|
||||
|
||||
def test_security(self):
|
||||
issue = {"title": "security: fix XSS in gallery panel"}
|
||||
assert categorize_issue(issue) == "security"
|
||||
|
||||
def test_governance(self):
|
||||
issue = {"title": "[AUDIT] Triage the backlog"}
|
||||
assert categorize_issue(issue) == "governance"
|
||||
|
||||
def test_research(self):
|
||||
issue = {"title": "research: investigate model drift"}
|
||||
assert categorize_issue(issue) == "research"
|
||||
|
||||
def test_epic(self):
|
||||
issue = {"title": "[EPIC] Contraction sweep across all repos"}
|
||||
assert categorize_issue(issue) == "epic"
|
||||
|
||||
def test_other(self):
|
||||
issue = {"title": "chore: cleanup whitespace"}
|
||||
assert categorize_issue(issue) == "other"
|
||||
|
||||
def test_case_insensitive(self):
|
||||
issue = {"title": "FIX: resolve import error"}
|
||||
assert categorize_issue(issue) == "bug"
|
||||
|
||||
def test_empty_title(self):
|
||||
issue = {"title": ""}
|
||||
assert categorize_issue(issue) == "other"
|
||||
|
||||
def test_none_title(self):
|
||||
issue = {}
|
||||
assert categorize_issue(issue) == "other"
|
||||
|
||||
|
||||
class TestExtractRefs:
|
||||
def test_single_ref(self):
|
||||
issue = {"title": "Fix #123", "body": "Closes #123"}
|
||||
assert extract_refs(issue) == [123]
|
||||
|
||||
def test_multiple_refs(self):
|
||||
issue = {"title": "Fix #123", "body": "Related to #456 and #789"}
|
||||
assert extract_refs(issue) == [123, 456, 789]
|
||||
|
||||
def test_deduplication(self):
|
||||
issue = {"title": "#100", "body": "Fixes #100"}
|
||||
assert extract_refs(issue) == [100]
|
||||
|
||||
def test_no_refs(self):
|
||||
issue = {"title": "No issue here", "body": "Just an issue"}
|
||||
assert extract_refs(issue) == []
|
||||
|
||||
def test_empty_body(self):
|
||||
issue = {"title": "Fix #42", "body": None}
|
||||
assert extract_refs(issue) == [42]
|
||||
|
||||
def test_numeric_like_text_not_refs(self):
|
||||
issue = {"title": "Version 2.0 release", "body": "See build #1234"}
|
||||
assert extract_refs(issue) == [1234]
|
||||
|
||||
|
||||
class TestFindDuplicates:
|
||||
def test_no_duplicates(self):
|
||||
issues = [{"number": 1, "title": "Fix #10", "body": ""},
|
||||
{"number": 2, "title": "Fix #11", "body": ""}]
|
||||
assert find_duplicates(issues) == {}
|
||||
|
||||
def test_duplicates_found(self):
|
||||
issues = [{"number": 1, "title": "Fix #10", "body": ""},
|
||||
{"number": 2, "title": "Also fix #10", "body": ""}]
|
||||
dupes = find_duplicates(issues)
|
||||
assert 10 in dupes
|
||||
assert dupes[10] == [1, 2]
|
||||
|
||||
def test_triple_duplicate(self):
|
||||
issues = [{"number": 1, "title": "#42", "body": ""},
|
||||
{"number": 2, "title": "#42", "body": ""},
|
||||
{"number": 3, "title": "#42", "body": ""}]
|
||||
dupes = find_duplicates(issues)
|
||||
assert len(dupes[42]) == 3
|
||||
|
||||
def test_partial_overlap(self):
|
||||
issues = [{"number": 1, "title": "#10 #20", "body": ""},
|
||||
{"number": 2, "title": "#10", "body": ""}]
|
||||
dupes = find_duplicates(issues)
|
||||
assert 10 in dupes
|
||||
assert 20 not in dupes
|
||||
|
||||
|
||||
class TestIsStale:
|
||||
def test_fresh_issue(self):
|
||||
now = datetime.now(timezone.utc)
|
||||
issue = {
|
||||
"number": 1,
|
||||
"title": "Fresh",
|
||||
"updated_at": now.isoformat(),
|
||||
"created_at": now.isoformat(),
|
||||
}
|
||||
assert not is_stale(issue, now - timedelta(days=STALE_DAYS))
|
||||
|
||||
def test_old_issue(self):
|
||||
old = datetime.now(timezone.utc) - timedelta(days=STALE_DAYS + 1)
|
||||
issue = {
|
||||
"number": 2,
|
||||
"title": "Old",
|
||||
"updated_at": old.isoformat(),
|
||||
"created_at": old.isoformat(),
|
||||
}
|
||||
assert is_stale(issue, datetime.now(timezone.utc) - timedelta(days=STALE_DAYS))
|
||||
Reference in New Issue
Block a user