Files
compounding-intelligence/scripts/test_priority_rebalancer.py
Alexander Whitestone 341abab2a0 feat: Priority Rebalancer — re-score issues from pipeline data (#174)
Monthly pipeline tool that:
- Reads knowledge store, metrics, and staleness data
- Scores all open issues across the org
- Suggests priority upgrades/downgrades based on accumulated signals
- Generates JSON + markdown reports
- Optional --apply mode to push changes via Gitea API

Signals detected:
- Stale/missing knowledge entries
- Empty knowledge store
- Missing metrics output
- Low repo coverage
- Issue age, activity, assignment status
- Keyword/label analysis

Usage:
  python3 scripts/priority_rebalancer.py --org Timmy_Foundation
  python3 scripts/priority_rebalancer.py --org Timmy_Foundation --apply
  python3 scripts/priority_rebalancer.py --org Timmy_Foundation --json

23 tests, all passing.
2026-04-15 10:52:51 -04:00

306 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""
Tests for Priority Rebalancer
"""
import json
import os
import sys
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
# Add script dir to path
sys.path.insert(0, str(Path(__file__).parent))
from priority_rebalancer import (
GiteaClient,
IssueScore,
PipelineSignal,
compute_issue_score,
collect_knowledge_signals,
collect_metrics_signals,
extract_priority,
generate_report,
generate_markdown_report,
PRIORITY_LEVELS,
)
# ============================================================
# Test Helpers
# ============================================================
PASS = 0
FAIL = 0
def test(name):
def decorator(fn):
global PASS, FAIL
try:
fn()
PASS += 1
print(f" [PASS] {name}")
except Exception as e:
FAIL += 1
print(f" [FAIL] {name}: {e}")
return decorator
def assert_eq(a, b, msg=""):
if a != b:
raise AssertionError(f"{msg} expected {b!r}, got {a!r}")
def assert_true(v, msg=""):
if not v:
raise AssertionError(msg or "Expected True")
def assert_false(v, msg=""):
if v:
raise AssertionError(msg or "Expected False")
# ============================================================
# Priority Extraction Tests
# ============================================================
print("=== Priority Rebalancer Tests ===\n")
print("-- Priority Extraction --")
@test("extract P0 from label")
def _():
assert_eq(extract_priority(["P0", "bug"]), "P0")
@test("extract P1 from priority:high")
def _():
assert_eq(extract_priority(["priority:high"]), "P1")
@test("extract P2 from priority:medium")
def _():
assert_eq(extract_priority(["priority:medium"]), "P2")
@test("extract P3 from priority:low")
def _():
assert_eq(extract_priority(["priority:low"]), "P3")
@test("returns None for no priority")
def _():
assert_eq(extract_priority(["bug", "enhancement"]), None)
@test("case insensitive")
def _():
assert_eq(extract_priority(["p1"]), "P1")
assert_eq(extract_priority(["PRIORITY:CRITICAL"]), "P0")
# ============================================================
# Issue Scoring Tests
# ============================================================
print("\n-- Issue Scoring --")
def make_issue(**kwargs):
defaults = {
"number": 1,
"title": "Test issue",
"labels": [],
"created_at": (datetime.utcnow() - timedelta(days=5)).isoformat() + "Z",
"comments": 0,
"assignees": None,
}
defaults.update(kwargs)
return defaults
@test("bug gets score boost")
def _():
issue = make_issue(title="Incorrect output format", labels=["bug"])
score = compute_issue_score(issue, "test-repo", [], datetime.utcnow())
assert_true(score.score > 0, f"Bug should boost score, got {score.score}")
# Bug label alone should be P2 or P3 (not P0)
assert_true(score.suggested_priority in ("P2", "P3"),
f"Bug label alone should be P2/P3, got {score.suggested_priority}")
@test("security gets high score")
def _():
issue = make_issue(title="Security: auth bypass", labels=["bug"])
score = compute_issue_score(issue, "test-repo", [], datetime.utcnow())
assert_true(score.score >= 25, f"Security should score high, got {score.score}")
@test("old dormant issue gets penalized")
def _():
issue = make_issue(
title="Some old feature",
created_at=(datetime.utcnow() - timedelta(days=120)).isoformat() + "Z",
comments=0
)
score = compute_issue_score(issue, "test-repo", [], datetime.utcnow())
assert_true(score.score < 0, f"Old dormant should be negative, got {score.score}")
assert_true(any("Dormant" in r for r in score.reasons), "Should mention dormancy")
@test("active discussion boosts score")
def _():
issue = make_issue(title="Important fix", comments=8)
score = compute_issue_score(issue, "test-repo", [], datetime.utcnow())
assert_true(score.score > 5, f"Active discussion should boost, got {score.score}")
assert_true(any("Active" in r for r in score.reasons))
@test("unassigned gets slight boost")
def _():
issue = make_issue(title="Fix bug", assignees=None)
score = compute_issue_score(issue, "test-repo", [], datetime.utcnow())
assert_true(any("Unassigned" in r for r in score.reasons))
@test("assigned issue notes assignee")
def _():
issue = make_issue(title="Fix bug", assignees=[{"login": "alice"}])
score = compute_issue_score(issue, "test-repo", [], datetime.utcnow())
assert_eq(score.assignee, "alice")
@test("nice-to-have gets penalized")
def _():
issue = make_issue(title="Nice to have: fancy animation")
score = compute_issue_score(issue, "test-repo", [], datetime.utcnow())
assert_true(score.score < 0, f"Nice-to-have should be negative, got {score.score}")
# ============================================================
# Pipeline Signal Tests
# ============================================================
print("\n-- Pipeline Signals --")
@test("signal alignment boosts matching issues")
def _():
signals = [PipelineSignal(
source="knowledge",
signal_type="stale_knowledge",
weight=0.8,
detail="20 stale facts"
)]
issue = make_issue(title="Fix stale knowledge entries")
score = compute_issue_score(issue, "test-repo", signals, datetime.utcnow())
assert_true(any("Matches signal" in r for r in score.reasons))
@test("empty knowledge boosts harvester issues")
def _():
signals = [PipelineSignal(
source="knowledge",
signal_type="empty_knowledge",
weight=0.7,
detail="0 facts"
)]
issue = make_issue(title="Implement harvester pipeline")
score = compute_issue_score(issue, "test-repo", signals, datetime.utcnow())
assert_true(any("Critical gap" in r for r in score.reasons))
# ============================================================
# Knowledge Signal Collection Tests
# ============================================================
print("\n-- Knowledge Signal Collection --")
@test("missing index generates signal")
def _():
with tempfile.TemporaryDirectory() as tmpdir:
signals = collect_knowledge_signals(tmpdir)
assert_true(len(signals) > 0)
assert_eq(signals[0].signal_type, "missing_index")
@test("empty knowledge generates signal")
def _():
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": []}, f)
signals = collect_knowledge_signals(tmpdir)
assert_true(any(s.signal_type == "empty_knowledge" for s in signals))
@test("corrupt index generates signal")
def _():
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
f.write("not json {{{")
signals = collect_knowledge_signals(tmpdir)
assert_true(any(s.signal_type == "corrupt_index" for s in signals))
@test("knowledge with facts passes")
def _():
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": [
{"id": 1, "repo": "test", "status": "fresh"},
{"id": 2, "repo": "test", "status": "fresh"},
]}, f)
signals = collect_knowledge_signals(tmpdir)
# Should not generate missing_index or empty_knowledge
assert_false(any(s.signal_type in ("missing_index", "empty_knowledge") for s in signals))
# ============================================================
# Metrics Signal Collection Tests
# ============================================================
print("\n-- Metrics Signal Collection --")
@test("empty metrics dir generates signal")
def _():
with tempfile.TemporaryDirectory() as tmpdir:
signals = collect_metrics_signals(tmpdir)
assert_true(any(s.signal_type == "no_metrics" for s in signals))
@test("metrics with files passes")
def _():
with tempfile.TemporaryDirectory() as tmpdir:
# Create files (simulating real metrics dir with .gitkeep + actual files)
with open(os.path.join(tmpdir, ".gitkeep"), "w") as f:
f.write("")
with open(os.path.join(tmpdir, "report.json"), "w") as f:
f.write("{}")
signals = collect_metrics_signals(tmpdir)
assert_false(any(s.signal_type == "no_metrics" for s in signals))
# ============================================================
# Report Generation Tests
# ============================================================
print("\n-- Report Generation --")
@test("report has correct structure")
def _():
scores = [
IssueScore(1, "repo1", "Bug fix", ["bug"], None, "P1", 30.0, ["test"], 5, 3, None),
IssueScore(2, "repo1", "Feature", ["enhancement"], "P3", None, -5.0, ["test"], 60, 0, "alice"),
]
signals = [PipelineSignal("knowledge", "stale_knowledge", 0.5, "10 stale")]
report = generate_report(scores, signals, "test-org", ["repo1"])
assert_eq(report["org"], "test-org")
assert_eq(report["total_issues"], 2)
assert_true("generated_at" in report)
assert_true("summary" in report)
assert_true("top_priority" in report)
assert_eq(report["summary"]["suggested_new_priorities"], 1)
@test("markdown report is non-empty")
def _():
scores = [IssueScore(1, "repo1", "Test", ["bug"], None, "P2", 15.0, ["reason"], 5, 0, None)]
report = generate_report(scores, [], "test-org", ["repo1"])
md = generate_markdown_report(report)
assert_true(len(md) > 100)
assert_true("Priority Rebalancer Report" in md)
assert_true("Top 10" in md)
# ============================================================
# Summary
# ============================================================
print(f"\n=== Summary ===")
print(f"Total: {PASS + FAIL} | Passed: {PASS} | Failed: {FAIL}")
if FAIL > 0:
sys.exit(1)