diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 094447fb..3a452d49 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,8 +29,8 @@ jobs: - name: Run tests run: | + mkdir -p reports pytest \ - --tb=short \ --cov=src \ --cov-report=term-missing \ --cov-report=xml:reports/coverage.xml \ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09708db0..596cf77d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,12 +51,13 @@ repos: exclude: ^tests/ stages: [manual] - # Fast unit tests only (not E2E, not slow tests) + # Full test suite with 30-second wall-clock limit. + # Current baseline: ~18s. If tests get slow, this blocks the commit. - repo: local hooks: - - id: pytest-unit - name: pytest-unit - entry: pytest + - id: pytest-fast + name: pytest (30s limit) + entry: timeout 30 poetry run pytest language: system types: [python] stages: [commit] @@ -64,8 +65,7 @@ repos: always_run: true args: - tests - - -m - - "unit" - - --tb=short - -q + - --tb=short + - --timeout=10 verbose: true diff --git a/Makefile b/Makefile index 346a7f4b..32538592 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install install-bigbrain dev nuke fresh test test-cov test-cov-html watch lint clean help \ +.PHONY: install install-bigbrain install-hooks dev nuke fresh test test-cov test-cov-html watch lint clean help \ up down logs \ docker-build docker-up docker-down docker-agent docker-logs docker-shell \ test-docker test-docker-cov test-docker-functional test-docker-build test-docker-down \ @@ -16,6 +16,11 @@ install: poetry install --with dev @echo "✓ Ready. Run 'make dev' to start the dashboard." +install-hooks: + cp scripts/pre-commit-hook.sh .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + @echo "✓ Pre-commit hook installed (30s test time limit)." + install-bigbrain: poetry install --with dev --extras bigbrain @if [ "$$(uname -m)" = "arm64" ] && [ "$$(uname -s)" = "Darwin" ]; then \ diff --git a/pyproject.toml b/pyproject.toml index 9980af68..8ee00eaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,10 @@ testpaths = ["tests"] pythonpath = ["src", "tests"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" -addopts = "-v --tb=short --timeout=30" +timeout = 30 +timeout_method = "signal" +timeout_func_only = false +addopts = "-v --tb=short --strict-markers --disable-warnings -n auto --dist worksteal" markers = [ "unit: Unit tests (fast, no I/O)", "integration: Integration tests (may use SQLite)", @@ -90,6 +93,7 @@ markers = [ "selenium: Requires Selenium and Chrome (browser automation)", "docker: Requires Docker and docker-compose", "ollama: Requires Ollama service running", + "external_api: Requires external API access", "skip_ci: Skip in CI environment (local development only)", ] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index a87584d5..00000000 --- a/pytest.ini +++ /dev/null @@ -1,58 +0,0 @@ -[pytest] -# Test discovery and execution configuration -testpaths = tests -pythonpath = src:tests -asyncio_mode = auto -asyncio_default_fixture_loop_scope = function -timeout = 30 -timeout_method = signal -timeout_func_only = false - -# Test markers for categorization and selective execution -markers = - unit: Unit tests (fast, no I/O, no external services) - integration: Integration tests (may use SQLite, in-process agents) - functional: Functional tests (real HTTP requests, no mocking) - e2e: End-to-end tests (full system, may be slow) - slow: Tests that take >1 second - selenium: Requires Selenium and Chrome (browser automation) - docker: Requires Docker and docker-compose - ollama: Requires Ollama service running - external_api: Requires external API access - skip_ci: Skip in CI environment (local development only) - -# Output and reporting -# -n auto: run tests in parallel across all CPU cores (pytest-xdist) -# Override with -n0 to disable parallelism for debugging -addopts = - -v - --tb=short - --strict-markers - --disable-warnings - -n auto - --dist worksteal - -# Coverage configuration -[coverage:run] -source = src -omit = - */tests/* - */site-packages/* - -[coverage:report] -show_missing = true -skip_empty = true -precision = 1 -exclude_lines = - pragma: no cover - if __name__ == .__main__. - if TYPE_CHECKING: - raise NotImplementedError - @abstractmethod -fail_under = 60 - -[coverage:html] -directory = htmlcov - -[coverage:xml] -output = coverage.xml diff --git a/scripts/pre-commit-hook.sh b/scripts/pre-commit-hook.sh new file mode 100755 index 00000000..c12e2d80 --- /dev/null +++ b/scripts/pre-commit-hook.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Pre-commit hook: run tests with a wall-clock limit. +# Blocks the commit if tests fail or take too long. +# Current baseline: ~18s wall-clock. Limit set to 30s for headroom. + +MAX_SECONDS=30 + +echo "Running tests (${MAX_SECONDS}s limit)..." + +timeout "${MAX_SECONDS}" poetry run pytest tests -q --tb=short --timeout=10 +exit_code=$? + +if [ "$exit_code" -eq 124 ]; then + echo "" + echo "BLOCKED: tests exceeded ${MAX_SECONDS}s wall-clock limit." + echo "Speed up slow tests before committing." + exit 1 +elif [ "$exit_code" -ne 0 ]; then + echo "" + echo "BLOCKED: tests failed." + exit 1 +fi diff --git a/src/brain/__init__.py b/src/brain/__init__.py index c89df8be..5555b48a 100644 --- a/src/brain/__init__.py +++ b/src/brain/__init__.py @@ -14,7 +14,6 @@ from brain.client import BrainClient from brain.worker import DistributedWorker from brain.embeddings import LocalEmbedder from brain.memory import UnifiedMemory, get_memory -from brain.identity import get_canonical_identity, get_identity_for_prompt __all__ = [ "BrainClient", @@ -22,6 +21,4 @@ __all__ = [ "LocalEmbedder", "UnifiedMemory", "get_memory", - "get_canonical_identity", - "get_identity_for_prompt", ] diff --git a/src/brain/client.py b/src/brain/client.py index 9f54fdd8..d0855124 100644 --- a/src/brain/client.py +++ b/src/brain/client.py @@ -36,7 +36,7 @@ class BrainClient: """Detect what component is using the brain.""" # Could be 'timmy', 'zeroclaw', 'worker', etc. # For now, infer from context or env - return os.environ.get("BRAIN_SOURCE", "timmy") + return os.environ.get("BRAIN_SOURCE", "default") # ────────────────────────────────────────────────────────────────────────── # Memory Operations diff --git a/src/brain/identity.py b/src/brain/identity.py deleted file mode 100644 index 94ec3e4f..00000000 --- a/src/brain/identity.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Identity loader — stripped. - -The persona/identity system has been removed. These functions remain -as no-op stubs so that call-sites don't break at import time. -""" - -from __future__ import annotations - -import logging -from typing import Optional - -logger = logging.getLogger(__name__) - - -def get_canonical_identity(force_refresh: bool = False) -> str: - """Return empty string — identity system removed.""" - return "" - - -def get_identity_section(section_name: str) -> str: - """Return empty string — identity system removed.""" - return "" - - -def get_identity_for_prompt(include_sections: Optional[list[str]] = None) -> str: - """Return empty string — identity system removed.""" - return "" - - -def get_agent_roster() -> list[dict[str, str]]: - """Return empty list — identity system removed.""" - return [] - - -_FALLBACK_IDENTITY = "" diff --git a/src/brain/memory.py b/src/brain/memory.py index 148857d0..43f138a4 100644 --- a/src/brain/memory.py +++ b/src/brain/memory.py @@ -65,7 +65,7 @@ class UnifiedMemory: def __init__( self, db_path: Optional[Path] = None, - source: str = "timmy", + source: str = "default", use_rqlite: Optional[bool] = None, ): self.db_path = db_path or _get_db_path() diff --git a/src/config.py b/src/config.py index cd601758..794b4197 100644 --- a/src/config.py +++ b/src/config.py @@ -4,6 +4,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + # Display name for the primary agent — override with AGENT_NAME env var + agent_name: str = "Agent" + # Ollama host — override with OLLAMA_URL env var or .env file ollama_url: str = "http://localhost:11434" diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 2f03a9fe..89c51f42 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -206,7 +206,7 @@ async def lifespan(app: FastAPI): # Start chat integrations in background chat_task = asyncio.create_task(_start_chat_integrations_background()) - logger.info("✓ Timmy Time dashboard ready for requests") + logger.info("✓ Dashboard ready for requests") yield @@ -227,7 +227,7 @@ async def lifespan(app: FastAPI): app = FastAPI( - title="Timmy Time — Mission Control", + title="Mission Control", version="1.0.0", lifespan=lifespan, docs_url="/docs", diff --git a/src/dashboard/routes/agents.py b/src/dashboard/routes/agents.py index 38361afa..aef925a9 100644 --- a/src/dashboard/routes/agents.py +++ b/src/dashboard/routes/agents.py @@ -4,7 +4,7 @@ from datetime import datetime from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse -from timmy.session import chat as timmy_chat +from timmy.session import chat as agent_chat from dashboard.store import message_log from dashboard.templating import templates @@ -21,8 +21,8 @@ async def list_agents(): return { "agents": [ { - "id": "orchestrator", - "name": "Orchestrator", + "id": "default", + "name": settings.agent_name, "status": "idle", "capabilities": "chat,reasoning,research,planning", "type": "local", @@ -34,15 +34,15 @@ async def list_agents(): } -@router.get("/timmy/panel", response_class=HTMLResponse) -async def timmy_panel(request: Request): +@router.get("/default/panel", response_class=HTMLResponse) +async def agent_panel(request: Request): """Chat panel — for HTMX main-panel swaps.""" return templates.TemplateResponse( - request, "partials/timmy_panel.html", {"agent": None} + request, "partials/agent_panel_chat.html", {"agent": None} ) -@router.get("/timmy/history", response_class=HTMLResponse) +@router.get("/default/history", response_class=HTMLResponse) async def get_history(request: Request): return templates.TemplateResponse( request, @@ -51,7 +51,7 @@ async def get_history(request: Request): ) -@router.delete("/timmy/history", response_class=HTMLResponse) +@router.delete("/default/history", response_class=HTMLResponse) async def clear_history(request: Request): message_log.clear() return templates.TemplateResponse( @@ -61,15 +61,15 @@ async def clear_history(request: Request): ) -@router.post("/timmy/chat", response_class=HTMLResponse) -async def chat_timmy(request: Request, message: str = Form(...)): +@router.post("/default/chat", response_class=HTMLResponse) +async def chat_agent(request: Request, message: str = Form(...)): """Chat — synchronous response.""" timestamp = datetime.now().strftime("%H:%M:%S") response_text = None error_text = None try: - response_text = timmy_chat(message) + response_text = agent_chat(message) except Exception as exc: logger.error("Chat error: %s", exc) error_text = f"Chat error: {exc}" diff --git a/src/dashboard/routes/chat_api.py b/src/dashboard/routes/chat_api.py index a189e69d..59c69008 100644 --- a/src/dashboard/routes/chat_api.py +++ b/src/dashboard/routes/chat_api.py @@ -20,7 +20,7 @@ from fastapi.responses import JSONResponse from config import settings from dashboard.store import message_log -from timmy.session import chat as timmy_chat +from timmy.session import chat as agent_chat logger = logging.getLogger(__name__) @@ -80,7 +80,7 @@ async def api_chat(request: Request): f"{now.strftime('%A, %B %d, %Y at %I:%M %p')}]\n" f"[System: Mobile client]\n\n" ) - response_text = timmy_chat( + response_text = agent_chat( context_prefix + last_user_msg, session_id="mobile", ) diff --git a/src/dashboard/routes/discord.py b/src/dashboard/routes/discord.py index c8e54a08..781f789e 100644 --- a/src/dashboard/routes/discord.py +++ b/src/dashboard/routes/discord.py @@ -115,7 +115,7 @@ async def join_from_image( result["oauth2_url"] = oauth_url result["message"] = ( "Invite validated. Share this OAuth2 URL with the server admin " - "to add Timmy to the server." + "to add the agent to the server." ) else: result["message"] = ( diff --git a/src/dashboard/routes/grok.py b/src/dashboard/routes/grok.py index 2115589c..ddd600e4 100644 --- a/src/dashboard/routes/grok.py +++ b/src/dashboard/routes/grok.py @@ -82,7 +82,7 @@ async def toggle_grok_mode(request: Request): import json spark_engine.on_tool_executed( - agent_id="timmy", + agent_id="default", tool_name="grok_mode_toggle", success=True, ) diff --git a/src/dashboard/routes/health.py b/src/dashboard/routes/health.py index e0e3dfba..6a6b3764 100644 --- a/src/dashboard/routes/health.py +++ b/src/dashboard/routes/health.py @@ -211,7 +211,7 @@ async def health_check(): # Legacy format for test compatibility ollama_ok = await check_ollama() - timmy_status = "idle" if ollama_ok else "offline" + agent_status = "idle" if ollama_ok else "offline" return { "status": "ok" if ollama_ok else "degraded", @@ -219,7 +219,7 @@ async def health_check(): "ollama": "up" if ollama_ok else "down", }, "agents": { - "timmy": {"status": timmy_status}, + "agent": {"status": agent_status}, }, # Extended fields for Mission Control "timestamp": datetime.now(timezone.utc).isoformat(), diff --git a/src/dashboard/routes/mobile.py b/src/dashboard/routes/mobile.py index 653282b3..3c903374 100644 --- a/src/dashboard/routes/mobile.py +++ b/src/dashboard/routes/mobile.py @@ -45,7 +45,7 @@ async def mobile_local_dashboard(request: Request): "browser_model_id": settings.browser_model_id, "browser_model_fallback": settings.browser_model_fallback, "server_model": settings.ollama_model, - "page_title": "Timmy — Local AI", + "page_title": "Local AI", }, ) @@ -71,7 +71,7 @@ async def mobile_status(): return { "ollama": "up" if ollama_ok else "down", "model": settings.ollama_model, - "agent": "timmy", + "agent": "default", "ready": True, "browser_model_enabled": settings.browser_model_enabled, "browser_model_id": settings.browser_model_id, diff --git a/src/dashboard/routes/tasks_celery.py b/src/dashboard/routes/tasks_celery.py index 5fbc9f77..2fb4b6c7 100644 --- a/src/dashboard/routes/tasks_celery.py +++ b/src/dashboard/routes/tasks_celery.py @@ -49,7 +49,7 @@ async def tasks_api(): async def submit_task_api(request: Request): """Submit a new background task. - Body: {"prompt": "...", "agent_id": "timmy"} + Body: {"prompt": "...", "agent_id": "default"} """ from infrastructure.celery.client import submit_chat_task @@ -62,7 +62,7 @@ async def submit_task_api(request: Request): if not prompt: return JSONResponse({"error": "prompt is required"}, status_code=400) - agent_id = body.get("agent_id", "timmy") + agent_id = body.get("agent_id", "default") task_id = submit_chat_task(prompt=prompt, agent_id=agent_id) if task_id is None: diff --git a/src/dashboard/routes/voice.py b/src/dashboard/routes/voice.py index cea86047..7482c10a 100644 --- a/src/dashboard/routes/voice.py +++ b/src/dashboard/routes/voice.py @@ -101,7 +101,7 @@ async def process_voice_input( try: if intent.name == "status": - response_text = "Timmy is operational and running locally. All systems sovereign." + response_text = "Agent is operational and running locally. All systems nominal." elif intent.name == "help": response_text = ( diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html index e90594dc..984ee5f3 100644 --- a/src/dashboard/templates/base.html +++ b/src/dashboard/templates/base.html @@ -6,7 +6,7 @@ - {% block title %}Timmy Time — Mission Control{% endblock %} + {% block title %}Mission Control{% endblock %} @@ -21,7 +21,7 @@
- TIMMY TIME + MISSION CONTROL MISSION CONTROL
diff --git a/src/dashboard/templates/briefing.html b/src/dashboard/templates/briefing.html index b85039e5..1fb98a34 100644 --- a/src/dashboard/templates/briefing.html +++ b/src/dashboard/templates/briefing.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Timmy Time — Morning Briefing{% endblock %} +{% block title %}Morning Briefing{% endblock %} {% block extra_styles %}