[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:
2026-04-26 14:54:15 -04:00
committed by Alexander Payne
parent 475b472929
commit 6b387af87f
2 changed files with 452 additions and 0 deletions

View 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))