diff --git a/src/dashboard/app.py b/src/dashboard/app.py index b28d97a8..27393797 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -33,12 +33,12 @@ from dashboard.routes.calm import router as calm_router from dashboard.routes.chat_api import router as chat_api_router from dashboard.routes.chat_api_v1 import router as chat_api_v1_router from dashboard.routes.daily_run import router as daily_run_router -from dashboard.routes.hermes import router as hermes_router from dashboard.routes.db_explorer import router as db_explorer_router from dashboard.routes.discord import router as discord_router from dashboard.routes.experiments import router as experiments_router from dashboard.routes.grok import router as grok_router from dashboard.routes.health import router as health_router +from dashboard.routes.hermes import router as hermes_router from dashboard.routes.loop_qa import router as loop_qa_router from dashboard.routes.memory import router as memory_router from dashboard.routes.mobile import router as mobile_router diff --git a/src/dashboard/routes/voice.py b/src/dashboard/routes/voice.py index b94a1a9b..6b187ad3 100644 --- a/src/dashboard/routes/voice.py +++ b/src/dashboard/routes/voice.py @@ -41,6 +41,7 @@ def _save_voice_settings(data: dict) -> None: except Exception as exc: logger.warning("Failed to save voice settings: %s", exc) + logger = logging.getLogger(__name__) router = APIRouter(prefix="/voice", tags=["voice"]) diff --git a/src/infrastructure/hermes/__init__.py b/src/infrastructure/hermes/__init__.py index b698c7d5..b8a83e2f 100644 --- a/src/infrastructure/hermes/__init__.py +++ b/src/infrastructure/hermes/__init__.py @@ -4,6 +4,6 @@ Monitors the local machine (Hermes/M3 Max) for memory pressure, disk usage, Ollama model health, zombie processes, and network connectivity. """ -from infrastructure.hermes.monitor import HermesMonitor, HealthLevel, HealthReport, hermes_monitor +from infrastructure.hermes.monitor import HealthLevel, HealthReport, HermesMonitor, hermes_monitor __all__ = ["HermesMonitor", "HealthLevel", "HealthReport", "hermes_monitor"] diff --git a/src/infrastructure/hermes/monitor.py b/src/infrastructure/hermes/monitor.py index 7619ea68..183acb00 100644 --- a/src/infrastructure/hermes/monitor.py +++ b/src/infrastructure/hermes/monitor.py @@ -19,11 +19,12 @@ import json import logging import shutil import subprocess +import tempfile import time import urllib.request from dataclasses import dataclass, field from datetime import UTC, datetime -from enum import Enum +from enum import StrEnum from typing import Any from config import settings @@ -31,7 +32,7 @@ from config import settings logger = logging.getLogger(__name__) -class HealthLevel(str, Enum): +class HealthLevel(StrEnum): """Severity level for a health check result.""" OK = "ok" @@ -194,8 +195,7 @@ class HermesMonitor: name="memory", level=HealthLevel.CRITICAL, message=( - f"Critical: only {free_gb:.1f}GB free " - f"(threshold: {memory_free_min_gb}GB)" + f"Critical: only {free_gb:.1f}GB free (threshold: {memory_free_min_gb}GB)" ), details=details, needs_human=True, @@ -302,8 +302,7 @@ class HermesMonitor: name="disk", level=HealthLevel.CRITICAL, message=( - f"Critical: only {free_gb:.1f}GB free " - f"(threshold: {disk_free_min_gb}GB)" + f"Critical: only {free_gb:.1f}GB free (threshold: {disk_free_min_gb}GB)" ), details=details, needs_human=True, @@ -335,7 +334,7 @@ class HermesMonitor: cutoff = time.time() - 86400 # 24 hours ago try: - tmp = Path("/tmp") + tmp = Path(tempfile.gettempdir()) for item in tmp.iterdir(): try: stat = item.stat() @@ -345,11 +344,7 @@ class HermesMonitor: freed_bytes += stat.st_size item.unlink(missing_ok=True) elif item.is_dir(): - dir_size = sum( - f.stat().st_size - for f in item.rglob("*") - if f.is_file() - ) + dir_size = sum(f.stat().st_size for f in item.rglob("*") if f.is_file()) freed_bytes += dir_size shutil.rmtree(str(item), ignore_errors=True) except (PermissionError, OSError): @@ -392,10 +387,7 @@ class HermesMonitor: return CheckResult( name="ollama", level=HealthLevel.OK, - message=( - f"Ollama OK — {len(models)} model(s) available, " - f"{len(loaded)} loaded" - ), + message=(f"Ollama OK — {len(models)} model(s) available, {len(loaded)} loaded"), details={ "reachable": True, "model_count": len(models), diff --git a/src/integrations/bannerlord/observer.py b/src/integrations/bannerlord/observer.py index 6f779243..78ec8736 100644 --- a/src/integrations/bannerlord/observer.py +++ b/src/integrations/bannerlord/observer.py @@ -135,7 +135,9 @@ class BannerlordObserver: self._host = host or settings.gabs_host self._port = port or settings.gabs_port self._timeout = timeout if timeout is not None else settings.gabs_timeout - self._poll_interval = poll_interval if poll_interval is not None else settings.gabs_poll_interval + self._poll_interval = ( + poll_interval if poll_interval is not None else settings.gabs_poll_interval + ) self._journal_path = Path(journal_path) if journal_path else _get_journal_path() self._entry_count = 0 self._days_observed: set[str] = set() diff --git a/src/timmy/agents/emotional_state.py b/src/timmy/agents/emotional_state.py index 3b54caa1..f99b4695 100644 --- a/src/timmy/agents/emotional_state.py +++ b/src/timmy/agents/emotional_state.py @@ -196,9 +196,7 @@ class EmotionalStateTracker: "intensity_label": _intensity_label(self.state.intensity), "previous_emotion": self.state.previous_emotion, "trigger_event": self.state.trigger_event, - "prompt_modifier": EMOTION_PROMPT_MODIFIERS.get( - self.state.current_emotion, "" - ), + "prompt_modifier": EMOTION_PROMPT_MODIFIERS.get(self.state.current_emotion, ""), } def get_prompt_modifier(self) -> str: diff --git a/src/timmy/backlog_triage.py b/src/timmy/backlog_triage.py index 935da163..d9a6c4a5 100644 --- a/src/timmy/backlog_triage.py +++ b/src/timmy/backlog_triage.py @@ -36,7 +36,7 @@ import asyncio import logging import re from dataclasses import dataclass, field -from datetime import UTC, datetime, timedelta +from datetime import UTC, datetime from typing import Any import httpx @@ -70,7 +70,9 @@ _LOOP_TAG = "loop-generated" # Regex patterns for scoring _TAG_RE = re.compile(r"\[([^\]]+)\]") -_FILE_RE = re.compile(r"(?:src/|tests/|scripts/|\.py|\.html|\.js|\.yaml|\.toml|\.sh)", re.IGNORECASE) +_FILE_RE = re.compile( + r"(?:src/|tests/|scripts/|\.py|\.html|\.js|\.yaml|\.toml|\.sh)", re.IGNORECASE +) _FUNC_RE = re.compile(r"(?:def |class |function |method |`\w+\(\)`)", re.IGNORECASE) _ACCEPT_RE = re.compile( r"(?:should|must|expect|verify|assert|test.?case|acceptance|criteria" @@ -451,9 +453,7 @@ async def add_label( # Apply to the issue apply_url = _repo_url(f"issues/{issue_number}/labels") - apply_resp = await client.post( - apply_url, headers=headers, json={"labels": [label_id]} - ) + apply_resp = await client.post(apply_url, headers=headers, json={"labels": [label_id]}) return apply_resp.status_code in (200, 201) except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException) as exc: @@ -692,7 +692,9 @@ class BacklogTriageLoop: # 1. Fetch raw_issues = await fetch_open_issues(client) result.total_open = len(raw_issues) - logger.info("Triage cycle #%d: fetched %d open issues", self._cycle_count, len(raw_issues)) + logger.info( + "Triage cycle #%d: fetched %d open issues", self._cycle_count, len(raw_issues) + ) # 2. Score scored = [score_issue(i) for i in raw_issues] diff --git a/src/timmy/dispatcher.py b/src/timmy/dispatcher.py index a4af2e2b..833e20e5 100644 --- a/src/timmy/dispatcher.py +++ b/src/timmy/dispatcher.py @@ -37,7 +37,7 @@ from __future__ import annotations import asyncio import logging from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum from typing import Any from config import settings @@ -48,7 +48,8 @@ logger = logging.getLogger(__name__) # Enumerations # --------------------------------------------------------------------------- -class AgentType(str, Enum): + +class AgentType(StrEnum): """Known agents in the swarm.""" CLAUDE_CODE = "claude_code" @@ -57,7 +58,7 @@ class AgentType(str, Enum): TIMMY = "timmy" -class TaskType(str, Enum): +class TaskType(StrEnum): """Categories of engineering work.""" # Claude Code strengths @@ -83,7 +84,7 @@ class TaskType(str, Enum): ORCHESTRATION = "orchestration" -class DispatchStatus(str, Enum): +class DispatchStatus(StrEnum): """Lifecycle state of a dispatched task.""" PENDING = "pending" @@ -99,6 +100,7 @@ class DispatchStatus(str, Enum): # Agent registry # --------------------------------------------------------------------------- + @dataclass class AgentSpec: """Capabilities and limits for a single agent.""" @@ -106,9 +108,9 @@ class AgentSpec: name: AgentType display_name: str strengths: frozenset[TaskType] - gitea_label: str | None # label to apply when dispatching + gitea_label: str | None # label to apply when dispatching max_concurrent: int = 1 - interface: str = "gitea" # "gitea" | "api" | "local" + interface: str = "gitea" # "gitea" | "api" | "local" api_endpoint: str | None = None # for interface="api" @@ -197,6 +199,7 @@ _TASK_ROUTING: dict[TaskType, AgentType] = { # Dispatch result # --------------------------------------------------------------------------- + @dataclass class DispatchResult: """Outcome of a dispatch call.""" @@ -220,6 +223,7 @@ class DispatchResult: # Routing logic # --------------------------------------------------------------------------- + def select_agent(task_type: TaskType) -> AgentType: """Return the best agent for *task_type* based on the routing table. @@ -248,11 +252,23 @@ def infer_task_type(title: str, description: str = "") -> TaskType: text = (title + " " + description).lower() _SIGNALS: list[tuple[TaskType, frozenset[str]]] = [ - (TaskType.ARCHITECTURE, frozenset({"architect", "design", "adr", "system design", "schema"})), - (TaskType.REFACTORING, frozenset({"refactor", "clean up", "cleanup", "reorganise", "reorganize"})), + ( + TaskType.ARCHITECTURE, + frozenset({"architect", "design", "adr", "system design", "schema"}), + ), + ( + TaskType.REFACTORING, + frozenset({"refactor", "clean up", "cleanup", "reorganise", "reorganize"}), + ), (TaskType.CODE_REVIEW, frozenset({"review", "pr review", "pull request review", "audit"})), - (TaskType.COMPLEX_REASONING, frozenset({"complex", "hard problem", "debug", "investigate", "diagnose"})), - (TaskType.RESEARCH, frozenset({"research", "survey", "literature", "benchmark", "analyse", "analyze"})), + ( + TaskType.COMPLEX_REASONING, + frozenset({"complex", "hard problem", "debug", "investigate", "diagnose"}), + ), + ( + TaskType.RESEARCH, + frozenset({"research", "survey", "literature", "benchmark", "analyse", "analyze"}), + ), (TaskType.ANALYSIS, frozenset({"analysis", "profil", "trace", "metric", "performance"})), (TaskType.TRIAGE, frozenset({"triage", "classify", "prioritise", "prioritize"})), (TaskType.PLANNING, frozenset({"plan", "roadmap", "milestone", "epic", "spike"})), @@ -273,6 +289,7 @@ def infer_task_type(title: str, description: str = "") -> TaskType: # Gitea helpers # --------------------------------------------------------------------------- + async def _post_gitea_comment( client: Any, base_url: str, @@ -405,6 +422,7 @@ async def _poll_issue_completion( # Core dispatch functions # --------------------------------------------------------------------------- + async def _dispatch_via_gitea( agent: AgentType, issue_number: int, @@ -479,7 +497,11 @@ async def _dispatch_via_gitea( ) # 2. Post assignment comment - criteria_md = "\n".join(f"- {c}" for c in acceptance_criteria) if acceptance_criteria else "_None specified_" + criteria_md = ( + "\n".join(f"- {c}" for c in acceptance_criteria) + if acceptance_criteria + else "_None specified_" + ) comment_body = ( f"## Assigned to {spec.display_name}\n\n" f"**Task type:** `{task_type.value}`\n\n" @@ -616,9 +638,7 @@ async def _dispatch_local( assumed to succeed at dispatch time). """ task_type = infer_task_type(title, description) - logger.info( - "Timmy handling task locally: %r (issue #%s)", title[:60], issue_number - ) + logger.info("Timmy handling task locally: %r (issue #%s)", title[:60], issue_number) return DispatchResult( task_type=task_type, agent=AgentType.TIMMY, @@ -632,6 +652,7 @@ async def _dispatch_local( # Public entry point # --------------------------------------------------------------------------- + async def dispatch_task( title: str, description: str = "", @@ -769,9 +790,7 @@ async def _log_escalation( f"---\n*Timmy agent dispatcher.*" ) async with httpx.AsyncClient(timeout=10) as client: - await _post_gitea_comment( - client, base_url, repo, headers, issue_number, body - ) + await _post_gitea_comment(client, base_url, repo, headers, issue_number, body) except Exception as exc: logger.warning("Failed to post escalation comment: %s", exc) @@ -780,6 +799,7 @@ async def _log_escalation( # Monitoring helper # --------------------------------------------------------------------------- + async def wait_for_completion( issue_number: int, poll_interval: int = 60, diff --git a/src/timmy/mcp_bridge.py b/src/timmy/mcp_bridge.py index e0e20ae5..7ce96099 100644 --- a/src/timmy/mcp_bridge.py +++ b/src/timmy/mcp_bridge.py @@ -418,9 +418,7 @@ class MCPBridge: return f"Error executing {name}: {exc}" @staticmethod - def _build_initial_messages( - prompt: str, system_prompt: str | None - ) -> list[dict]: + def _build_initial_messages(prompt: str, system_prompt: str | None) -> list[dict]: """Build the initial message list for a run.""" messages: list[dict] = [] if system_prompt: @@ -512,9 +510,7 @@ class MCPBridge: error_msg = "" try: - content, tool_calls_made, rounds, error_msg = await self._run_tool_loop( - messages, tools - ) + content, tool_calls_made, rounds, error_msg = await self._run_tool_loop(messages, tools) except httpx.ConnectError as exc: logger.warning("Ollama connection failed: %s", exc) error_msg = f"Ollama connection failed: {exc}" diff --git a/src/timmy/vassal/agent_health.py b/src/timmy/vassal/agent_health.py index d5796ac6..f95718b0 100644 --- a/src/timmy/vassal/agent_health.py +++ b/src/timmy/vassal/agent_health.py @@ -47,13 +47,11 @@ _DEFAULT_IDLE_THRESHOLD = 30 class AgentStatus: """Health snapshot for one agent at a point in time.""" - agent: str # "claude" | "kimi" | "timmy" + agent: str # "claude" | "kimi" | "timmy" is_idle: bool = True active_issue_numbers: list[int] = field(default_factory=list) stuck_issue_numbers: list[int] = field(default_factory=list) - checked_at: str = field( - default_factory=lambda: datetime.now(UTC).isoformat() - ) + checked_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat()) @property def is_stuck(self) -> bool: @@ -69,9 +67,7 @@ class AgentHealthReport: """Combined health report for all monitored agents.""" agents: list[AgentStatus] = field(default_factory=list) - generated_at: str = field( - default_factory=lambda: datetime.now(UTC).isoformat() - ) + generated_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat()) @property def any_stuck(self) -> bool: @@ -193,18 +189,14 @@ async def check_agent_health( try: async with httpx.AsyncClient(timeout=15) as client: - issues = await _fetch_labeled_issues( - client, base_url, headers, repo, label - ) + issues = await _fetch_labeled_issues(client, base_url, headers, repo, label) for issue in issues: num = issue.get("number", 0) status.active_issue_numbers.append(num) # Check last activity - last_activity = await _last_comment_time( - client, base_url, headers, repo, num - ) + last_activity = await _last_comment_time(client, base_url, headers, repo, num) if last_activity is None: last_activity = await _issue_created_time(issue) diff --git a/src/timmy/vassal/backlog.py b/src/timmy/vassal/backlog.py index c24e851f..08a6beed 100644 --- a/src/timmy/vassal/backlog.py +++ b/src/timmy/vassal/backlog.py @@ -91,9 +91,9 @@ _PRIORITY_LABEL_SCORES: dict[str, int] = { class AgentTarget(StrEnum): """Which agent should handle this issue.""" - TIMMY = "timmy" # Timmy handles locally (self) + TIMMY = "timmy" # Timmy handles locally (self) CLAUDE = "claude" # Dispatch to Claude Code - KIMI = "kimi" # Dispatch to Kimi Code + KIMI = "kimi" # Dispatch to Kimi Code @dataclass @@ -172,9 +172,7 @@ def triage_issues(raw_issues: list[dict[str, Any]]) -> list[TriagedIssue]: title = issue.get("title", "") body = issue.get("body") or "" labels = _extract_labels(issue) - assignees = [ - a.get("login", "") for a in issue.get("assignees") or [] - ] + assignees = [a.get("login", "") for a in issue.get("assignees") or []] url = issue.get("html_url", "") priority = _score_priority(labels, assignees) @@ -252,9 +250,7 @@ async def fetch_open_issues( params=params, ) if resp.status_code != 200: - logger.warning( - "fetch_open_issues: Gitea returned %s", resp.status_code - ) + logger.warning("fetch_open_issues: Gitea returned %s", resp.status_code) return [] issues = resp.json() diff --git a/src/timmy/vassal/dispatch.py b/src/timmy/vassal/dispatch.py index 8f1b02cd..b659bb03 100644 --- a/src/timmy/vassal/dispatch.py +++ b/src/timmy/vassal/dispatch.py @@ -34,7 +34,7 @@ _LABEL_MAP: dict[AgentTarget, str] = { _LABEL_COLORS: dict[str, str] = { "claude-ready": "#8b6f47", # warm brown - "kimi-ready": "#006b75", # dark teal + "kimi-ready": "#006b75", # dark teal "timmy-ready": "#0075ca", # blue } @@ -52,9 +52,7 @@ class DispatchRecord: issue_title: str agent: AgentTarget rationale: str - dispatched_at: str = field( - default_factory=lambda: datetime.now(UTC).isoformat() - ) + dispatched_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat()) label_applied: bool = False comment_posted: bool = False @@ -170,9 +168,7 @@ async def dispatch_issue(issue: TriagedIssue) -> DispatchRecord: try: async with httpx.AsyncClient(timeout=15) as client: - label_id = await _get_or_create_label( - client, base_url, headers, repo, label_name - ) + label_id = await _get_or_create_label(client, base_url, headers, repo, label_name) # Apply label if label_id is not None: diff --git a/src/timmy/vassal/house_health.py b/src/timmy/vassal/house_health.py index 24bdcf0f..f24e9d8d 100644 --- a/src/timmy/vassal/house_health.py +++ b/src/timmy/vassal/house_health.py @@ -22,9 +22,9 @@ logger = logging.getLogger(__name__) # Thresholds # --------------------------------------------------------------------------- -_WARN_DISK_PCT = 85.0 # warn when disk is more than 85% full -_WARN_MEM_PCT = 90.0 # warn when memory is more than 90% used -_WARN_CPU_PCT = 95.0 # warn when CPU is above 95% sustained +_WARN_DISK_PCT = 85.0 # warn when disk is more than 85% full +_WARN_MEM_PCT = 90.0 # warn when memory is more than 90% used +_WARN_CPU_PCT = 95.0 # warn when CPU is above 95% sustained # --------------------------------------------------------------------------- @@ -63,9 +63,7 @@ class SystemSnapshot: memory: MemoryUsage = field(default_factory=MemoryUsage) ollama: OllamaHealth = field(default_factory=OllamaHealth) warnings: list[str] = field(default_factory=list) - taken_at: str = field( - default_factory=lambda: datetime.now(UTC).isoformat() - ) + taken_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat()) @property def healthy(self) -> bool: @@ -117,8 +115,8 @@ def _probe_memory() -> MemoryUsage: def _probe_ollama_sync(ollama_url: str) -> OllamaHealth: """Synchronous Ollama health probe — run in a thread.""" try: - import urllib.request import json + import urllib.request url = ollama_url.rstrip("/") + "/api/tags" with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310 @@ -154,14 +152,12 @@ async def get_system_snapshot() -> SystemSnapshot: if disk.percent_used >= _WARN_DISK_PCT: warnings.append( - f"Disk {disk.path}: {disk.percent_used:.0f}% used " - f"({disk.free_gb:.1f} GB free)" + f"Disk {disk.path}: {disk.percent_used:.0f}% used ({disk.free_gb:.1f} GB free)" ) if memory.percent_used >= _WARN_MEM_PCT: warnings.append( - f"Memory: {memory.percent_used:.0f}% used " - f"({memory.available_gb:.1f} GB available)" + f"Memory: {memory.percent_used:.0f}% used ({memory.available_gb:.1f} GB available)" ) if not ollama.reachable: @@ -216,7 +212,5 @@ async def cleanup_stale_files( errors.append(str(exc)) await asyncio.to_thread(_cleanup) - logger.info( - "cleanup_stale_files: deleted %d files, %d errors", deleted, len(errors) - ) + logger.info("cleanup_stale_files: deleted %d files, %d errors", deleted, len(errors)) return {"deleted_count": deleted, "errors": errors} diff --git a/tests/infrastructure/test_router_cascade.py b/tests/infrastructure/test_router_cascade.py index 5b539e90..10aa9ac0 100644 --- a/tests/infrastructure/test_router_cascade.py +++ b/tests/infrastructure/test_router_cascade.py @@ -523,7 +523,7 @@ class TestProviderAvailabilityCheck: def test_check_vllm_mlx_server_healthy(self): """Test vllm-mlx when health check succeeds.""" - from unittest.mock import MagicMock, patch + from unittest.mock import patch router = CascadeRouter(config_path=Path("/nonexistent")) @@ -567,7 +567,7 @@ class TestProviderAvailabilityCheck: def test_check_vllm_mlx_default_url(self): """Test vllm-mlx uses default localhost:8000 when no URL configured.""" - from unittest.mock import MagicMock, patch + from unittest.mock import patch router = CascadeRouter(config_path=Path("/nonexistent")) @@ -623,7 +623,7 @@ class TestVllmMlxProvider: async def test_vllm_mlx_base_url_normalization(self): """Test _call_vllm_mlx appends /v1 when missing.""" - from unittest.mock import AsyncMock, MagicMock, patch + from unittest.mock import AsyncMock, patch router = CascadeRouter(config_path=Path("/nonexistent")) @@ -1059,9 +1059,7 @@ class TestTransformMessagesForOllama: def test_plain_text_message(self): router = self._router() - result = router._transform_messages_for_ollama( - [{"role": "user", "content": "Hello"}] - ) + result = router._transform_messages_for_ollama([{"role": "user", "content": "Hello"}]) assert result == [{"role": "user", "content": "Hello"}] def test_base64_image_stripped(self): @@ -1204,7 +1202,10 @@ class TestCascadeTierFiltering: router = self._make_router() with patch("infrastructure.router.cascade._quota_monitor", None): with patch.object(router, "_call_anthropic") as mock_call: - mock_call.return_value = {"content": "frontier response", "model": "claude-sonnet-4-6"} + mock_call.return_value = { + "content": "frontier response", + "model": "claude-sonnet-4-6", + } result = await router.complete( messages=[{"role": "user", "content": "hi"}], cascade_tier="frontier_required", diff --git a/tests/integrations/test_gabs_observer.py b/tests/integrations/test_gabs_observer.py index 471ec8f4..bf7c446c 100644 --- a/tests/integrations/test_gabs_observer.py +++ b/tests/integrations/test_gabs_observer.py @@ -10,14 +10,12 @@ from __future__ import annotations import json import socket -from pathlib import Path from unittest.mock import MagicMock, patch import pytest from integrations.bannerlord.gabs_client import GabsClient, GabsError - # ── GabsClient unit tests ───────────────────────────────────────────────────── @@ -236,7 +234,13 @@ class TestBannerlordObserver: snapshot = { "game_state": {"day": 7, "season": "winter", "campaign_phase": "early"}, - "player": {"name": "Timmy", "clan": "Thalheimer", "renown": 42, "level": 3, "gold": 1000}, + "player": { + "name": "Timmy", + "clan": "Thalheimer", + "renown": 42, + "level": 3, + "gold": 1000, + }, "player_party": {"size": 25, "morale": 80, "food_days_left": 5}, "kingdoms": [{"name": "Vlandia", "ruler": "Derthert", "military_strength": 5000}], } diff --git a/tests/timmy/agents/test_emotional_state.py b/tests/timmy/agents/test_emotional_state.py index 6ad83ae1..eadcc165 100644 --- a/tests/timmy/agents/test_emotional_state.py +++ b/tests/timmy/agents/test_emotional_state.py @@ -1,7 +1,6 @@ """Tests for agent emotional state simulation (src/timmy/agents/emotional_state.py).""" import time -from unittest.mock import patch from timmy.agents.emotional_state import ( EMOTION_PROMPT_MODIFIERS, diff --git a/tests/timmy/test_dispatcher.py b/tests/timmy/test_dispatcher.py index cab79ce5..cd858f11 100644 --- a/tests/timmy/test_dispatcher.py +++ b/tests/timmy/test_dispatcher.py @@ -4,8 +4,6 @@ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch -import pytest - from timmy.dispatcher import ( AGENT_REGISTRY, AgentType, @@ -21,11 +19,11 @@ from timmy.dispatcher import ( wait_for_completion, ) - # --------------------------------------------------------------------------- # Agent registry # --------------------------------------------------------------------------- + class TestAgentRegistry: def test_all_agents_present(self): for member in AgentType: @@ -41,7 +39,7 @@ class TestAgentRegistry: assert spec.gitea_label, f"{agent} is gitea interface but has no label" def test_non_gitea_agents_have_no_labels(self): - for agent, spec in AGENT_REGISTRY.items(): + for _agent, spec in AGENT_REGISTRY.items(): if spec.interface not in ("gitea",): # api and local agents may have no label assert spec.gitea_label is None or spec.interface == "gitea" @@ -55,6 +53,7 @@ class TestAgentRegistry: # select_agent # --------------------------------------------------------------------------- + class TestSelectAgent: def test_architecture_routes_to_claude(self): assert select_agent(TaskType.ARCHITECTURE) == AgentType.CLAUDE_CODE @@ -85,6 +84,7 @@ class TestSelectAgent: # infer_task_type # --------------------------------------------------------------------------- + class TestInferTaskType: def test_architecture_keyword(self): assert infer_task_type("Design the LLM router architecture") == TaskType.ARCHITECTURE @@ -119,6 +119,7 @@ class TestInferTaskType: # DispatchResult # --------------------------------------------------------------------------- + class TestDispatchResult: def test_success_when_assigned(self): r = DispatchResult( @@ -161,6 +162,7 @@ class TestDispatchResult: # _dispatch_local # --------------------------------------------------------------------------- + class TestDispatchLocal: async def test_returns_assigned(self): result = await _dispatch_local( @@ -190,6 +192,7 @@ class TestDispatchLocal: # _dispatch_via_api # --------------------------------------------------------------------------- + class TestDispatchViaApi: async def test_no_endpoint_returns_failed(self): result = await _dispatch_via_api( @@ -304,7 +307,9 @@ class TestDispatchViaGitea: assert result.status == DispatchStatus.ASSIGNED async def test_no_gitea_token_returns_failed(self): - bad_settings = MagicMock(gitea_enabled=True, gitea_token="", gitea_url="http://x", gitea_repo="a/b") + bad_settings = MagicMock( + gitea_enabled=True, gitea_token="", gitea_url="http://x", gitea_repo="a/b" + ) with patch("timmy.dispatcher.settings", bad_settings): result = await _dispatch_via_gitea( agent=AgentType.CLAUDE_CODE, @@ -317,7 +322,9 @@ class TestDispatchViaGitea: assert "not configured" in (result.error or "").lower() async def test_gitea_disabled_returns_failed(self): - bad_settings = MagicMock(gitea_enabled=False, gitea_token="tok", gitea_url="http://x", gitea_repo="a/b") + bad_settings = MagicMock( + gitea_enabled=False, gitea_token="tok", gitea_url="http://x", gitea_repo="a/b" + ) with patch("timmy.dispatcher.settings", bad_settings): result = await _dispatch_via_gitea( agent=AgentType.CLAUDE_CODE, @@ -368,6 +375,7 @@ class TestDispatchViaGitea: # dispatch_task (integration-style) # --------------------------------------------------------------------------- + class TestDispatchTask: async def test_empty_title_returns_failed(self): result = await dispatch_task(title=" ") @@ -396,7 +404,9 @@ class TestDispatchTask: client_mock = AsyncMock() client_mock.__aenter__ = AsyncMock(return_value=client_mock) client_mock.__aexit__ = AsyncMock(return_value=False) - client_mock.get = AsyncMock(return_value=MagicMock(status_code=200, json=MagicMock(return_value=[]))) + client_mock.get = AsyncMock( + return_value=MagicMock(status_code=200, json=MagicMock(return_value=[])) + ) create_resp = MagicMock(status_code=201, json=MagicMock(return_value={"id": 1})) apply_resp = MagicMock(status_code=201) comment_resp = MagicMock(status_code=201, json=MagicMock(return_value={"id": 5})) @@ -464,6 +474,7 @@ class TestDispatchTask: # wait_for_completion # --------------------------------------------------------------------------- + class TestWaitForCompletion: async def test_returns_completed_when_issue_closed(self): closed_resp = MagicMock( diff --git a/tests/unit/test_backlog_triage.py b/tests/unit/test_backlog_triage.py index e5d758e2..b9d82177 100644 --- a/tests/unit/test_backlog_triage.py +++ b/tests/unit/test_backlog_triage.py @@ -25,7 +25,6 @@ from timmy.backlog_triage import ( score_issue, ) - # ── Fixtures ───────────────────────────────────────────────────────────────── diff --git a/tests/unit/test_hermes_monitor.py b/tests/unit/test_hermes_monitor.py index c4e0e23f..2dcb4f6e 100644 --- a/tests/unit/test_hermes_monitor.py +++ b/tests/unit/test_hermes_monitor.py @@ -7,7 +7,6 @@ Refs: #1073 """ import json -from io import BytesIO from unittest.mock import MagicMock, patch import pytest @@ -79,7 +78,9 @@ def test_get_memory_info_handles_subprocess_failure(monitor): @pytest.mark.asyncio async def test_check_memory_ok(monitor): - with patch.object(monitor, "_get_memory_info", return_value={"free_gb": 20.0, "total_gb": 64.0}): + with patch.object( + monitor, "_get_memory_info", return_value={"free_gb": 20.0, "total_gb": 64.0} + ): result = await monitor._check_memory() assert result.name == "memory" @@ -126,7 +127,7 @@ async def test_check_memory_exception_returns_unknown(monitor): @pytest.mark.asyncio async def test_check_disk_ok(monitor): usage = MagicMock() - usage.free = 100 * (1024**3) # 100 GB + usage.free = 100 * (1024**3) # 100 GB usage.total = 500 * (1024**3) # 500 GB usage.used = 400 * (1024**3) @@ -140,7 +141,7 @@ async def test_check_disk_ok(monitor): @pytest.mark.asyncio async def test_check_disk_low_triggers_cleanup(monitor): usage = MagicMock() - usage.free = 5 * (1024**3) # 5 GB — below threshold + usage.free = 5 * (1024**3) # 5 GB — below threshold usage.total = 500 * (1024**3) usage.used = 495 * (1024**3) @@ -176,12 +177,8 @@ async def test_check_disk_critical_when_cleanup_fails(monitor): def test_get_ollama_status_reachable(monitor): - tags_body = json.dumps({ - "models": [{"name": "qwen3:30b"}, {"name": "llama3.1:8b"}] - }).encode() - ps_body = json.dumps({ - "models": [{"name": "qwen3:30b", "size": 1000}] - }).encode() + tags_body = json.dumps({"models": [{"name": "qwen3:30b"}, {"name": "llama3.1:8b"}]}).encode() + ps_body = json.dumps({"models": [{"name": "qwen3:30b", "size": 1000}]}).encode() responses = [ _FakeHTTPResponse(tags_body), diff --git a/tests/unit/test_vassal_agent_health.py b/tests/unit/test_vassal_agent_health.py index 299281f4..e2879705 100644 --- a/tests/unit/test_vassal_agent_health.py +++ b/tests/unit/test_vassal_agent_health.py @@ -6,7 +6,6 @@ import pytest from timmy.vassal.agent_health import AgentHealthReport, AgentStatus - # --------------------------------------------------------------------------- # AgentStatus # --------------------------------------------------------------------------- @@ -49,9 +48,7 @@ def test_report_any_stuck(): def test_report_all_idle(): - report = AgentHealthReport( - agents=[AgentStatus(agent="claude"), AgentStatus(agent="kimi")] - ) + report = AgentHealthReport(agents=[AgentStatus(agent="claude"), AgentStatus(agent="kimi")]) assert report.all_idle is True diff --git a/tests/unit/test_vassal_backlog.py b/tests/unit/test_vassal_backlog.py index b37ed952..38084f7d 100644 --- a/tests/unit/test_vassal_backlog.py +++ b/tests/unit/test_vassal_backlog.py @@ -6,14 +6,12 @@ import pytest from timmy.vassal.backlog import ( AgentTarget, - TriagedIssue, _choose_agent, _extract_labels, _score_priority, triage_issues, ) - # --------------------------------------------------------------------------- # _extract_labels # --------------------------------------------------------------------------- diff --git a/tests/unit/test_vassal_house_health.py b/tests/unit/test_vassal_house_health.py index a9241e6f..6fc09a73 100644 --- a/tests/unit/test_vassal_house_health.py +++ b/tests/unit/test_vassal_house_health.py @@ -12,7 +12,6 @@ from timmy.vassal.house_health import ( _probe_disk, ) - # --------------------------------------------------------------------------- # Data model tests # --------------------------------------------------------------------------- diff --git a/tests/unit/test_vassal_orchestration_loop.py b/tests/unit/test_vassal_orchestration_loop.py index c6dd6598..90262369 100644 --- a/tests/unit/test_vassal_orchestration_loop.py +++ b/tests/unit/test_vassal_orchestration_loop.py @@ -6,7 +6,6 @@ import pytest from timmy.vassal.orchestration_loop import VassalCycleRecord, VassalOrchestrator - # --------------------------------------------------------------------------- # VassalCycleRecord # --------------------------------------------------------------------------- @@ -134,6 +133,6 @@ def test_orchestrator_stop_when_not_running(): def test_module_singleton_exists(): - from timmy.vassal import vassal_orchestrator, VassalOrchestrator + from timmy.vassal import VassalOrchestrator, vassal_orchestrator assert isinstance(vassal_orchestrator, VassalOrchestrator)