Files
Timmy-time-dashboard/tests/unit/test_vassal_backlog.py
Claude (Opus 4.6) 495c1ac2bd
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
[claude] Fix 27 ruff lint errors blocking all pushes (#1149) (#1153)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-23 19:06:11 +00:00

185 lines
5.5 KiB
Python

"""Unit tests for timmy.vassal.backlog — triage and fetch helpers."""
from __future__ import annotations
import pytest
from timmy.vassal.backlog import (
AgentTarget,
_choose_agent,
_extract_labels,
_score_priority,
triage_issues,
)
# ---------------------------------------------------------------------------
# _extract_labels
# ---------------------------------------------------------------------------
def test_extract_labels_empty():
assert _extract_labels({}) == []
def test_extract_labels_normalises_case():
issue = {"labels": [{"name": "HIGH"}, {"name": "Feature"}]}
assert _extract_labels(issue) == ["high", "feature"]
# ---------------------------------------------------------------------------
# _score_priority
# ---------------------------------------------------------------------------
def test_priority_urgent():
assert _score_priority(["urgent"], []) == 100
def test_priority_high():
assert _score_priority(["high"], []) == 75
def test_priority_normal_default():
assert _score_priority([], []) == 50
def test_priority_assigned_penalised():
# already assigned → subtract 20
score = _score_priority([], ["some-agent"])
assert score == 30
def test_priority_label_substring_match():
# "critical" contains "critical" → 90
assert _score_priority(["critical-bug"], []) == 90
# ---------------------------------------------------------------------------
# _choose_agent
# ---------------------------------------------------------------------------
def test_choose_claude_for_architecture():
target, rationale = _choose_agent("Refactor auth middleware", "", [])
assert target == AgentTarget.CLAUDE
assert "complex" in rationale or "high-complexity" in rationale
def test_choose_kimi_for_research():
target, rationale = _choose_agent("Deep research on embedding models", "", [])
assert target == AgentTarget.KIMI
def test_choose_timmy_for_docs():
target, rationale = _choose_agent("Update documentation for CLI", "", [])
assert target == AgentTarget.TIMMY
def test_choose_timmy_default():
target, rationale = _choose_agent("Fix typo in README", "simple change", [])
# Could route to timmy (docs/trivial) or default — either is valid
assert isinstance(target, AgentTarget)
def test_choose_agent_label_wins():
# "security" label → Claude
target, _ = _choose_agent("Login page", "", ["security"])
assert target == AgentTarget.CLAUDE
# ---------------------------------------------------------------------------
# triage_issues
# ---------------------------------------------------------------------------
def _make_raw_issue(
number: int,
title: str,
body: str = "",
labels: list[str] | None = None,
assignees: list[str] | None = None,
) -> dict:
return {
"number": number,
"title": title,
"body": body,
"labels": [{"name": lbl} for lbl in (labels or [])],
"assignees": [{"login": a} for a in (assignees or [])],
"html_url": f"http://gitea/issues/{number}",
}
def test_triage_returns_sorted_by_priority():
issues = [
_make_raw_issue(1, "Routine docs update", labels=["docs"]),
_make_raw_issue(2, "Critical security issue", labels=["urgent", "security"]),
_make_raw_issue(3, "Normal feature", labels=[]),
]
triaged = triage_issues(issues)
# Highest priority first
assert triaged[0].number == 2
assert triaged[0].priority_score == 100 # urgent label
def test_triage_prs_can_be_included():
# triage_issues does not filter PRs — that's fetch_open_issues's job
issues = [_make_raw_issue(10, "A PR-like issue")]
triaged = triage_issues(issues)
assert len(triaged) == 1
def test_triage_empty():
assert triage_issues([]) == []
def test_triage_routing():
issues = [
_make_raw_issue(1, "Benchmark LLM backends", body="comprehensive analysis"),
_make_raw_issue(2, "Refactor agent loader", body="architecture change"),
_make_raw_issue(3, "Fix typo in docs", labels=["docs"]),
]
triaged = {i.number: i for i in triage_issues(issues)}
assert triaged[1].agent_target == AgentTarget.KIMI
assert triaged[2].agent_target == AgentTarget.CLAUDE
assert triaged[3].agent_target == AgentTarget.TIMMY
def test_triage_preserves_url():
issues = [_make_raw_issue(42, "Some issue")]
triaged = triage_issues(issues)
assert triaged[0].url == "http://gitea/issues/42"
# ---------------------------------------------------------------------------
# fetch_open_issues — no Gitea available in unit tests
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_open_issues_returns_empty_when_disabled(monkeypatch):
"""When Gitea is disabled, fetch returns [] without raising."""
import timmy.vassal.backlog as bl
# Patch settings
class FakeSettings:
gitea_enabled = False
gitea_token = ""
gitea_url = "http://localhost:3000"
gitea_repo = "owner/repo"
monkeypatch.setattr(bl, "logger", bl.logger) # no-op just to confirm import
# We can't easily monkeypatch `from config import settings` inside the function,
# so test the no-token path via environment
import os
original = os.environ.pop("GITEA_TOKEN", None)
try:
result = await bl.fetch_open_issues()
# Should return [] gracefully (no token configured by default in test env)
assert isinstance(result, list)
finally:
if original is not None:
os.environ["GITEA_TOKEN"] = original