2026-03-07 23:48:20 -05:00
|
|
|
"""End-to-end tests for Round 4 bug fixes.
|
|
|
|
|
|
|
|
|
|
Covers: /calm, /api/queue/status, creative tabs, swarm live WS,
|
|
|
|
|
agent tools on /tools, notification bell /api/notifications,
|
|
|
|
|
and Ollama timeout parameter.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
from unittest.mock import MagicMock, patch
|
2026-03-07 23:48:20 -05:00
|
|
|
|
2026-03-11 18:36:42 -04:00
|
|
|
import pytest
|
|
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fix 1: /calm no longer returns 500
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_calm_page_returns_200(client):
|
|
|
|
|
"""GET /calm should render without error now that tables are created."""
|
|
|
|
|
response = client.get("/calm")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "Timmy Calm" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_calm_morning_ritual_form_returns_200(client):
|
|
|
|
|
"""GET /calm/ritual/morning loads the morning ritual form."""
|
|
|
|
|
response = client.get("/calm/ritual/morning")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fix 2: /api/queue/status endpoint exists
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_queue_status_returns_json(client):
|
|
|
|
|
"""GET /api/queue/status returns valid JSON instead of 404."""
|
|
|
|
|
response = client.get("/api/queue/status?assigned_to=default")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "is_working" in data
|
|
|
|
|
assert "current_task" in data
|
|
|
|
|
assert "tasks_ahead" in data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_queue_status_default_idle(client):
|
|
|
|
|
"""Queue status shows idle when no tasks are running."""
|
|
|
|
|
response = client.get("/api/queue/status")
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["is_working"] is False
|
|
|
|
|
assert data["current_task"] is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_queue_status_reflects_running_task(client):
|
|
|
|
|
"""Queue status shows working when a task is running."""
|
|
|
|
|
# Create a task and set it to running
|
2026-03-08 12:50:44 -04:00
|
|
|
create = client.post(
|
|
|
|
|
"/api/tasks",
|
|
|
|
|
json={
|
|
|
|
|
"title": "Running task",
|
|
|
|
|
"assigned_to": "default",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-03-07 23:48:20 -05:00
|
|
|
task_id = create.json()["id"]
|
|
|
|
|
client.patch(f"/api/tasks/{task_id}/status", json={"status": "approved"})
|
|
|
|
|
client.patch(f"/api/tasks/{task_id}/status", json={"status": "running"})
|
|
|
|
|
|
|
|
|
|
response = client.get("/api/queue/status?assigned_to=default")
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["is_working"] is True
|
|
|
|
|
assert data["current_task"]["title"] == "Running task"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fix 3: Bootstrap JS present in base.html (creative tabs)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_base_html_has_bootstrap_js(client):
|
|
|
|
|
"""base.html should include bootstrap.bundle.min.js for tab switching."""
|
|
|
|
|
response = client.get("/")
|
|
|
|
|
assert "bootstrap.bundle.min.js" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_creative_page_returns_200(client):
|
|
|
|
|
"""GET /creative/ui should load without error."""
|
|
|
|
|
response = client.get("/creative/ui")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
# Verify tab structure exists
|
|
|
|
|
assert 'data-bs-toggle="tab"' in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fix 4: Swarm Live WebSocket sends initial state
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_swarm_live_page_returns_200(client):
|
|
|
|
|
"""GET /swarm/live renders the live dashboard page."""
|
|
|
|
|
response = client.get("/swarm/live")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_swarm_live_websocket_sends_initial_state(client):
|
|
|
|
|
"""WebSocket at /swarm/live sends initial_state on connect."""
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
with client.websocket_connect("/swarm/live") as ws:
|
|
|
|
|
data = ws.receive_json()
|
2026-03-11 18:36:42 -04:00
|
|
|
# First message should be initial_state with swarm data
|
|
|
|
|
assert data.get("type") == "initial_state", f"Unexpected WS message: {data}"
|
|
|
|
|
payload = data.get("data", {})
|
|
|
|
|
assert "agents" in payload
|
|
|
|
|
assert "tasks" in payload
|
|
|
|
|
assert "auctions" in payload
|
2026-03-07 23:48:20 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fix 5: Agent tools populated on /tools page
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_tools_page_returns_200(client):
|
|
|
|
|
"""GET /tools loads successfully."""
|
|
|
|
|
response = client.get("/tools")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tools_page_shows_agent_capabilities(client):
|
|
|
|
|
"""GET /tools should show agent capabilities, not 'No agents registered'."""
|
|
|
|
|
response = client.get("/tools")
|
|
|
|
|
# The tools registry always has at least the built-in tools
|
|
|
|
|
# If tools are registered, we should NOT see the empty message
|
|
|
|
|
from timmy.tools import get_all_available_tools
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
if get_all_available_tools():
|
|
|
|
|
assert "No agents registered yet" not in response.text
|
|
|
|
|
assert "Timmy" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tools_api_stats_returns_json(client):
|
|
|
|
|
"""GET /tools/api/stats returns valid JSON."""
|
|
|
|
|
response = client.get("/tools/api/stats")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "available_tools" in data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fix 6: Notification bell dropdown + /api/notifications
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_notifications_api_returns_json(client):
|
|
|
|
|
"""GET /api/notifications returns a JSON array."""
|
|
|
|
|
response = client.get("/api/notifications")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert isinstance(data, list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_notifications_bell_dropdown_in_html(client):
|
|
|
|
|
"""The notification bell should have a dropdown container."""
|
|
|
|
|
response = client.get("/")
|
|
|
|
|
assert "notif-dropdown" in response.text
|
|
|
|
|
assert "notif-list" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fix 0b: Ollama timeout parameter
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_create_timmy_uses_timeout_not_request_timeout():
|
|
|
|
|
"""create_timmy() should pass timeout=300, not request_timeout."""
|
ruff (#169)
* polish: streamline nav, extract inline styles, improve tablet UX
- Restructure desktop nav from 8+ flat links + overflow dropdown into
5 grouped dropdowns (Core, Agents, Intel, System, More) matching
the mobile menu structure to reduce decision fatigue
- Extract all inline styles from mission_control.html and base.html
notification elements into mission-control.css with semantic classes
- Replace JS-built innerHTML with secure DOM construction in
notification loader and chat history
- Add CONNECTING state to connection indicator (amber) instead of
showing OFFLINE before WebSocket connects
- Add tablet breakpoint (1024px) with larger touch targets for
Apple Pencil / stylus use and safe-area padding for iPad toolbar
- Add active-link highlighting in desktop dropdown menus
- Rename "Mission Control" page title to "System Overview" to
disambiguate from the chat home page
- Add "Home — Timmy Time" page title to index.html
https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h
* fix(security): move auth-gate credentials to environment variables
Hardcoded username, password, and HMAC secret in auth-gate.py replaced
with os.environ lookups. Startup now refuses to run if any variable is
unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example.
https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h
* refactor(tooling): migrate from black+isort+bandit to ruff
Replace three separate linting/formatting tools with a single ruff
invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs),
.pre-commit-config.yaml, and CI workflow. Fixes all ruff errors
including unused imports, missing raise-from, and undefined names.
Ruff config maps existing bandit skips to equivalent S-rules.
https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
|
|
|
with (
|
|
|
|
|
patch("timmy.agent.Ollama") as mock_ollama,
|
|
|
|
|
patch("timmy.agent.SqliteDb"),
|
|
|
|
|
patch("timmy.agent.Agent"),
|
2026-03-08 12:50:44 -04:00
|
|
|
):
|
2026-03-07 23:48:20 -05:00
|
|
|
mock_ollama.return_value = MagicMock()
|
|
|
|
|
|
|
|
|
|
from timmy.agent import create_timmy
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
try:
|
|
|
|
|
create_timmy()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if mock_ollama.called:
|
|
|
|
|
_, kwargs = mock_ollama.call_args
|
2026-03-08 12:50:44 -04:00
|
|
|
assert "request_timeout" not in kwargs, "Should use 'timeout', not 'request_timeout'"
|
2026-03-07 23:48:20 -05:00
|
|
|
assert kwargs.get("timeout") == 300
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Task lifecycle e2e: create → approve → run → complete
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_task_full_lifecycle(client):
|
|
|
|
|
"""Test full task lifecycle: create → approve → running → completed."""
|
|
|
|
|
# Create
|
2026-03-08 12:50:44 -04:00
|
|
|
create = client.post(
|
|
|
|
|
"/api/tasks",
|
|
|
|
|
json={
|
|
|
|
|
"title": "Lifecycle test",
|
|
|
|
|
"priority": "high",
|
|
|
|
|
"assigned_to": "default",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-03-07 23:48:20 -05:00
|
|
|
assert create.status_code == 201
|
|
|
|
|
task_id = create.json()["id"]
|
|
|
|
|
|
|
|
|
|
# Should appear in pending
|
|
|
|
|
pending = client.get("/tasks/pending")
|
|
|
|
|
assert "Lifecycle test" in pending.text
|
|
|
|
|
|
|
|
|
|
# Approve
|
|
|
|
|
approve = client.patch(
|
|
|
|
|
f"/api/tasks/{task_id}/status",
|
|
|
|
|
json={"status": "approved"},
|
|
|
|
|
)
|
|
|
|
|
assert approve.status_code == 200
|
|
|
|
|
assert approve.json()["status"] == "approved"
|
|
|
|
|
|
|
|
|
|
# Should now appear in active
|
|
|
|
|
active = client.get("/tasks/active")
|
|
|
|
|
assert "Lifecycle test" in active.text
|
|
|
|
|
|
|
|
|
|
# Set running
|
|
|
|
|
client.patch(f"/api/tasks/{task_id}/status", json={"status": "running"})
|
|
|
|
|
|
|
|
|
|
# Complete
|
|
|
|
|
complete = client.patch(
|
|
|
|
|
f"/api/tasks/{task_id}/status",
|
|
|
|
|
json={"status": "completed"},
|
|
|
|
|
)
|
|
|
|
|
assert complete.status_code == 200
|
|
|
|
|
assert complete.json()["status"] == "completed"
|
|
|
|
|
assert complete.json()["completed_at"] is not None
|
|
|
|
|
|
|
|
|
|
# Should now appear in completed
|
|
|
|
|
completed = client.get("/tasks/completed")
|
|
|
|
|
assert "Lifecycle test" in completed.text
|
|
|
|
|
|
|
|
|
|
# Should no longer appear in pending or active
|
|
|
|
|
pending2 = client.get("/tasks/pending")
|
|
|
|
|
assert "Lifecycle test" not in pending2.text
|
|
|
|
|
active2 = client.get("/tasks/active")
|
|
|
|
|
assert "Lifecycle test" not in active2.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Pages that were broken — verify they return 200
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-08 12:50:44 -04:00
|
|
|
|
2026-03-11 18:36:42 -04:00
|
|
|
@pytest.mark.skip_ci
|
2026-03-07 23:48:20 -05:00
|
|
|
def test_all_dashboard_pages_return_200(client):
|
2026-03-11 18:36:42 -04:00
|
|
|
"""Smoke test: all main dashboard routes return 200 (needs Ollama for /thinking)."""
|
2026-03-07 23:48:20 -05:00
|
|
|
pages = [
|
|
|
|
|
"/",
|
|
|
|
|
"/tasks",
|
|
|
|
|
"/briefing",
|
|
|
|
|
"/thinking",
|
|
|
|
|
"/swarm/mission-control",
|
|
|
|
|
"/swarm/live",
|
|
|
|
|
"/swarm/events",
|
|
|
|
|
"/bugs",
|
|
|
|
|
"/tools",
|
|
|
|
|
"/lightning/ledger",
|
|
|
|
|
"/self-modify/queue",
|
|
|
|
|
"/self-coding",
|
|
|
|
|
"/hands",
|
|
|
|
|
"/creative/ui",
|
|
|
|
|
"/calm",
|
|
|
|
|
]
|
|
|
|
|
for page in pages:
|
|
|
|
|
response = client.get(page)
|
|
|
|
|
assert response.status_code == 200, f"{page} returned {response.status_code}"
|