diff --git a/pyproject.toml b/pyproject.toml index d617c350..b49140ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ asyncio_default_fixture_loop_scope = "function" timeout = 30 timeout_method = "signal" timeout_func_only = false -addopts = "-v --tb=short --strict-markers --disable-warnings --durations=10" +addopts = "-v --tb=short --strict-markers --disable-warnings --durations=10 --cov-fail-under=60" markers = [ "unit: Unit tests (fast, no I/O)", "integration: Integration tests (may use SQLite)", diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 25a78204..575bbdf4 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -384,11 +384,12 @@ def _startup_background_tasks() -> list[asyncio.Task]: ] try: from timmy.paperclip import start_paperclip_poller + bg_tasks.append(asyncio.create_task(start_paperclip_poller())) logger.info("Paperclip poller started") except ImportError: logger.debug("Paperclip module not found, skipping poller") - + return bg_tasks diff --git a/src/infrastructure/router/cascade.py b/src/infrastructure/router/cascade.py index 84f07e90..1cb9747a 100644 --- a/src/infrastructure/router/cascade.py +++ b/src/infrastructure/router/cascade.py @@ -114,7 +114,7 @@ class Provider: type: str # ollama, openai, anthropic enabled: bool priority: int - tier: str | None = None # e.g., "local", "standard_cloud", "frontier" + tier: str | None = None # e.g., "local", "standard_cloud", "frontier" url: str | None = None api_key: str | None = None base_url: str | None = None @@ -573,7 +573,6 @@ class CascadeRouter: if not providers: raise RuntimeError(f"No providers found for tier: {cascade_tier}") - for provider in providers: if not self._is_provider_available(provider): continue diff --git a/src/timmy/paperclip.py b/src/timmy/paperclip.py index c42e2ee7..4934f9ee 100644 --- a/src/timmy/paperclip.py +++ b/src/timmy/paperclip.py @@ -13,8 +13,8 @@ from dataclasses import dataclass import httpx from config import settings +from timmy.research_tools import get_llm_client, google_web_search from timmy.research_triage import triage_research_report -from timmy.research_tools import google_web_search, get_llm_client logger = logging.getLogger(__name__) @@ -52,10 +52,7 @@ class PaperclipClient: ) resp.raise_for_status() tasks = resp.json() - return [ - PaperclipTask(id=t["id"], kind=t["kind"], context=t["context"]) - for t in tasks - ] + return [PaperclipTask(id=t["id"], kind=t["kind"], context=t["context"]) for t in tasks] async def update_task_status( self, task_id: str, status: str, result: str | None = None @@ -98,7 +95,7 @@ class ResearchOrchestrator: async def run_research_pipeline(self, issue_title: str) -> str: """Run the research pipeline.""" search_results = await google_web_search(issue_title) - + llm_client = get_llm_client() response = await llm_client.completion( f"Summarize the following search results and generate a research report:\\n\\n{search_results}", @@ -123,7 +120,9 @@ class ResearchOrchestrator: comment += "Created the following issues:\\n" for result in triage_results: if result["gitea_issue"]: - comment += f"- #{result['gitea_issue']['number']}: {result['action_item'].title}\\n" + comment += ( + f"- #{result['gitea_issue']['number']}: {result['action_item'].title}\\n" + ) else: comment += "No new issues were created.\\n" @@ -172,4 +171,3 @@ async def start_paperclip_poller() -> None: if settings.paperclip_enabled: poller = PaperclipPoller() asyncio.create_task(poller.poll()) - diff --git a/src/timmy/research_tools.py b/src/timmy/research_tools.py index bf3963fa..7818d27f 100644 --- a/src/timmy/research_tools.py +++ b/src/timmy/research_tools.py @@ -6,7 +6,6 @@ import logging import os from typing import Any -from config import settings from serpapi import GoogleSearch logger = logging.getLogger(__name__) @@ -28,6 +27,7 @@ async def google_web_search(query: str) -> str: def get_llm_client() -> Any: """Get an LLM client.""" + # This is a placeholder. In a real application, this would return # a client for an LLM service like OpenAI, Anthropic, or a local # model. diff --git a/tests/scripts/test_export_trajectories.py b/tests/scripts/test_export_trajectories.py index f6ef580b..0abba019 100644 --- a/tests/scripts/test_export_trajectories.py +++ b/tests/scripts/test_export_trajectories.py @@ -9,10 +9,8 @@ import json from pathlib import Path import pytest - import scripts.export_trajectories as et - # ── Fixtures ────────────────────────────────────────────────────────────────── @@ -22,10 +20,30 @@ def simple_session(tmp_path: Path) -> Path: logs_dir = tmp_path / "logs" logs_dir.mkdir() entries = [ - {"type": "message", "role": "user", "content": "What time is it?", "timestamp": "2026-03-01T10:00:00"}, - {"type": "message", "role": "timmy", "content": "It is 10:00 AM.", "timestamp": "2026-03-01T10:00:01"}, - {"type": "message", "role": "user", "content": "Thanks!", "timestamp": "2026-03-01T10:00:05"}, - {"type": "message", "role": "timmy", "content": "You're welcome!", "timestamp": "2026-03-01T10:00:06"}, + { + "type": "message", + "role": "user", + "content": "What time is it?", + "timestamp": "2026-03-01T10:00:00", + }, + { + "type": "message", + "role": "timmy", + "content": "It is 10:00 AM.", + "timestamp": "2026-03-01T10:00:01", + }, + { + "type": "message", + "role": "user", + "content": "Thanks!", + "timestamp": "2026-03-01T10:00:05", + }, + { + "type": "message", + "role": "timmy", + "content": "You're welcome!", + "timestamp": "2026-03-01T10:00:06", + }, ] session_file = logs_dir / "session_2026-03-01.jsonl" session_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n") @@ -38,7 +56,12 @@ def tool_call_session(tmp_path: Path) -> Path: logs_dir = tmp_path / "logs" logs_dir.mkdir() entries = [ - {"type": "message", "role": "user", "content": "Read CLAUDE.md", "timestamp": "2026-03-01T10:00:00"}, + { + "type": "message", + "role": "user", + "content": "Read CLAUDE.md", + "timestamp": "2026-03-01T10:00:00", + }, { "type": "tool_call", "tool": "read_file", @@ -46,7 +69,12 @@ def tool_call_session(tmp_path: Path) -> Path: "result": "# CLAUDE.md content here", "timestamp": "2026-03-01T10:00:01", }, - {"type": "message", "role": "timmy", "content": "Here is the content.", "timestamp": "2026-03-01T10:00:02"}, + { + "type": "message", + "role": "timmy", + "content": "Here is the content.", + "timestamp": "2026-03-01T10:00:02", + }, ] session_file = logs_dir / "session_2026-03-01.jsonl" session_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n") @@ -236,7 +264,7 @@ def test_export_training_data_writes_jsonl(simple_session: Path, tmp_path: Path) count = et.export_training_data(logs_dir=simple_session, output_path=output) assert count == 2 assert output.exists() - lines = [json.loads(l) for l in output.read_text().splitlines() if l.strip()] + lines = [json.loads(line) for line in output.read_text().splitlines() if line.strip()] assert len(lines) == 2 for line in lines: assert "messages" in line @@ -270,16 +298,22 @@ def test_export_training_data_returns_zero_for_empty_logs(tmp_path: Path) -> Non @pytest.mark.unit def test_cli_missing_logs_dir(tmp_path: Path) -> None: - rc = et.main(["--logs-dir", str(tmp_path / "nonexistent"), "--output", str(tmp_path / "out.jsonl")]) + rc = et.main( + ["--logs-dir", str(tmp_path / "nonexistent"), "--output", str(tmp_path / "out.jsonl")] + ) assert rc == 1 @pytest.mark.unit def test_cli_exports_and_returns_zero(simple_session: Path, tmp_path: Path) -> None: output = tmp_path / "out.jsonl" - rc = et.main([ - "--logs-dir", str(simple_session), - "--output", str(output), - ]) + rc = et.main( + [ + "--logs-dir", + str(simple_session), + "--output", + str(output), + ] + ) assert rc == 0 assert output.exists() diff --git a/tests/unit/test_retrain_loop.py b/tests/unit/test_retrain_loop.py index 313b50cd..d778769f 100644 --- a/tests/unit/test_retrain_loop.py +++ b/tests/unit/test_retrain_loop.py @@ -9,19 +9,15 @@ Refs: #1105 from __future__ import annotations import json -import tempfile from datetime import UTC, datetime, timedelta from pathlib import Path -import pytest - from timmy_automations.retrain.quality_filter import QualityFilter, TrajectoryQuality from timmy_automations.retrain.retrain import RetrainOrchestrator from timmy_automations.retrain.training_dataset import TrainingDataset from timmy_automations.retrain.training_log import CycleMetrics, TrainingLog from timmy_automations.retrain.trajectory_exporter import Trajectory, TrajectoryExporter - # ── Fixtures ───────────────────────────────────────────────────────────────── @@ -382,7 +378,7 @@ class TestTrainingDataset: ds = TrainingDataset(repo_root=tmp_path) ds.append([self._make_result()], "2026-W12") with open(ds.dataset_path) as f: - lines = [l.strip() for l in f if l.strip()] + lines = [line.strip() for line in f if line.strip()] assert len(lines) == 1 record = json.loads(lines[0]) assert "messages" in record