[gemini] feat: add coverage and duration strictness to pytest (#934) #1140
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user