feat: scaffold Timmy Time Mission Control (v1.0.0 Genesis)

- src/timmy/ — Agno agent wrapper (llama3.2 via Ollama, SQLite memory, TIMMY_SYSTEM_PROMPT)
- src/dashboard/ — FastAPI + HTMX + Jinja2 Mission Control UI
  - /health + /health/status (Ollama ping, HTMX 30s poll)
  - /agents list + /agents/timmy/chat (HTMX form submission)
- static/style.css — dark terminal mission-control aesthetic
- tests/ — 27 pytest tests (prompts, agent config, dashboard routes); no Ollama required
- pyproject.toml — hatchling build, pytest configured with pythonpath=src

https://claude.ai/code/session_01M4L3R98N5fgXFZRvV8X9b6
This commit is contained in:
Claude
2026-02-19 19:05:01 +00:00
parent 6b5e3f694c
commit 5e7d805245
23 changed files with 993 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Python
__pycache__/
*.py[cod]
*.pyo
.Python
build/
dist/
*.egg-info/
.eggs/
# Virtual envs
.venv/
venv/
env/
# Secrets / local config
.env
.env.*
# SQLite memory — never commit agent memory
*.db
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store

56
README.md Normal file
View File

@@ -0,0 +1,56 @@
# Timmy Time — Mission Control
Sovereign AI agent dashboard. Monitor and interact with local and cloud AI agents.
## Stack
| Layer | Tech |
|-----------|------------------------------|
| Agent | Agno + Ollama (llama3.2) |
| Memory | SQLite via Agno SqliteDb |
| Backend | FastAPI |
| Frontend | HTMX + Jinja2 |
| Tests | Pytest |
## Quickstart
```bash
pip install -e ".[dev]"
# Ollama (separate terminal)
ollama serve && ollama pull llama3.2
# Dashboard
uvicorn dashboard.app:app --reload
# Tests (no Ollama needed)
pytest
```
## CLI
```bash
timmy chat "What is sovereignty?"
timmy think "Bitcoin and self-custody"
timmy status
```
## Project Structure
```
src/
timmy/ # Agent identity — soul (prompt) + body (Agno)
dashboard/ # Mission Control UI
routes/ # FastAPI route handlers
templates/ # Jinja2 HTML (HTMX-powered)
static/ # CSS
tests/ # Pytest suite
```
## Roadmap
| Version | Name | Milestone |
|---------|------------|--------------------------------------------|
| 1.0.0 | Genesis | Agno + Ollama + SQLite + Dashboard |
| 2.0.0 | Exodus | MCP tools + multi-agent |
| 3.0.0 | Revelation | Bitcoin Lightning treasury + single `.app` |

47
STATUS.md Normal file
View File

@@ -0,0 +1,47 @@
# Timmy Time — Status
## Current Version: 1.0.0 (Genesis)
### What's Built
- `src/timmy/` — Agno-powered Timmy agent (llama3.2 via Ollama, SQLite memory)
- `src/dashboard/` — FastAPI Mission Control dashboard (HTMX + Jinja2)
- CLI: `timmy think / chat / status`
- Pytest test suite (prompts, agent config, dashboard routes)
### System Requirements
- Python 3.11+
- Ollama running at `http://localhost:11434`
- `llama3.2` model pulled
### Quickstart
```bash
pip install -e ".[dev]"
# Start Ollama (separate terminal)
ollama serve
ollama pull llama3.2
# Run dashboard
uvicorn dashboard.app:app --reload
# Run tests (no Ollama required)
pytest
```
### Dashboard
`http://localhost:8000` — Mission Control UI with:
- Timmy agent status panel
- Ollama health indicator (auto-refreshes every 30s)
- Live chat interface
---
## Roadmap
| Tag | Name | Milestone |
|-------|------------|----------------------------------------------|
| 1.0.0 | Genesis | Agno + Ollama + SQLite + Dashboard |
| 2.0.0 | Exodus | MCP tools + multi-agent support |
| 3.0.0 | Revelation | Bitcoin Lightning treasury + single `.app` |
_Last updated: 2026-02-19_

45
pyproject.toml Normal file
View File

@@ -0,0 +1,45 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "timmy-time"
version = "1.0.0"
description = "Mission Control for sovereign AI agents"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
dependencies = [
"agno>=1.4.0",
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"jinja2>=3.1.0",
"httpx>=0.27.0",
"python-multipart>=0.0.12",
"aiofiles>=24.0.0",
"typer>=0.12.0",
"rich>=13.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0",
"pytest-cov>=5.0.0",
]
[project.scripts]
timmy = "timmy.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/timmy", "src/dashboard"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
asyncio_mode = "auto"
addopts = "-v --tb=short"
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*"]

View File

25
src/dashboard/app.py Normal file
View File

@@ -0,0 +1,25 @@
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from dashboard.routes.agents import router as agents_router
from dashboard.routes.health import router as health_router
BASE_DIR = Path(__file__).parent
PROJECT_ROOT = BASE_DIR.parent.parent
app = FastAPI(title="Timmy Time — Mission Control", version="1.0.0")
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
app.mount("/static", StaticFiles(directory=str(PROJECT_ROOT / "static")), name="static")
app.include_router(health_router)
app.include_router(agents_router)
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse(request, "index.html")

View File

View File

@@ -0,0 +1,52 @@
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from timmy.agent import create_timmy
router = APIRouter(prefix="/agents", tags=["agents"])
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
AGENT_REGISTRY = {
"timmy": {
"id": "timmy",
"name": "Timmy",
"type": "sovereign",
"model": "llama3.2",
"backend": "ollama",
"version": "1.0.0",
}
}
@router.get("")
async def list_agents():
return {"agents": list(AGENT_REGISTRY.values())}
@router.post("/timmy/chat", response_class=HTMLResponse)
async def chat_timmy(request: Request, message: str = Form(...)):
timestamp = datetime.now().strftime("%H:%M:%S")
response_text = None
error_text = None
try:
agent = create_timmy()
run = agent.run(message, stream=False)
response_text = run.content if hasattr(run, "content") else str(run)
except Exception as exc:
error_text = f"Timmy is offline: {exc}"
return templates.TemplateResponse(
request,
"partials/chat_message.html",
{
"user_message": message,
"response": response_text,
"error": error_text,
"timestamp": timestamp,
},
)

View File

@@ -0,0 +1,42 @@
import httpx
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
router = APIRouter(tags=["health"])
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
OLLAMA_URL = "http://localhost:11434"
async def check_ollama() -> bool:
"""Ping Ollama to verify it's running."""
try:
async with httpx.AsyncClient(timeout=2.0) as client:
r = await client.get(OLLAMA_URL)
return r.status_code == 200
except Exception:
return False
@router.get("/health")
async def health():
ollama_ok = await check_ollama()
return {
"status": "ok",
"services": {
"ollama": "up" if ollama_ok else "down",
},
"agents": ["timmy"],
}
@router.get("/health/status", response_class=HTMLResponse)
async def health_status(request: Request):
ollama_ok = await check_ollama()
return templates.TemplateResponse(
request,
"partials/health_status.html",
{"ollama": ollama_ok},
)

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Timmy Time — Mission Control{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/style.css" />
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
</head>
<body>
<header class="mc-header">
<div class="mc-header-left">
<span class="mc-title">TIMMY TIME</span>
<span class="mc-subtitle">MISSION CONTROL</span>
</div>
<div class="mc-header-right">
<span class="mc-time" id="clock"></span>
</div>
</header>
<main class="mc-main">
{% block content %}{% endblock %}
</main>
<script>
function updateClock() {
const now = new Date();
document.getElementById('clock').textContent =
now.toLocaleTimeString('en-US', { hour12: false });
}
setInterval(updateClock, 1000);
updateClock();
</script>
</body>
</html>

View File

@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block content %}
<div class="sidebar">
<!-- Agents -->
<div class="panel">
<div class="panel-header">// AGENTS</div>
<div class="panel-body">
<div class="agent-card">
<div class="agent-card-header">
<span class="status-dot amber"></span>
<span class="agent-name">TIMMY</span>
</div>
<div class="agent-meta">
<span class="meta-key">TYPE</span> <span class="meta-val">sovereign</span><br>
<span class="meta-key">MODEL</span> <span class="meta-val">llama3.2</span><br>
<span class="meta-key">BACKEND</span> <span class="meta-val">ollama</span><br>
<span class="meta-key">VERSION</span> <span class="meta-val">1.0.0</span>
</div>
</div>
</div>
</div>
<!-- System Health (HTMX polled) -->
<div class="panel"
hx-get="/health/status"
hx-trigger="load, every 30s"
hx-target="this"
hx-swap="innerHTML">
<div class="panel-header">// SYSTEM HEALTH</div>
<div class="panel-body">
<div class="health-row">
<span class="health-label">LOADING...</span>
</div>
</div>
</div>
</div>
<!-- Chat Panel -->
<div class="panel chat-panel">
<div class="panel-header">// TIMMY INTERFACE</div>
<div class="chat-log" id="chat-log">
<div class="chat-message agent">
<div class="msg-meta">TIMMY // SYSTEM</div>
<div class="msg-body">Mission Control initialized. Timmy ready — awaiting input.</div>
</div>
</div>
<div class="chat-input-bar">
<form hx-post="/agents/timmy/chat"
hx-target="#chat-log"
hx-swap="beforeend"
hx-indicator="#send-indicator"
hx-on::after-request="this.reset(); scrollChat()"
style="display:flex; flex:1; gap:8px;">
<input type="text"
name="message"
placeholder="send a message to timmy..."
autocomplete="off"
required />
<button type="submit">
SEND
<span id="send-indicator" class="htmx-indicator"></span>
</button>
</form>
</div>
</div>
<script>
function scrollChat() {
const log = document.getElementById('chat-log');
log.scrollTop = log.scrollHeight;
}
scrollChat();
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
<div class="chat-message user">
<div class="msg-meta">YOU // {{ timestamp }}</div>
<div class="msg-body">{{ user_message }}</div>
</div>
{% if response %}
<div class="chat-message agent">
<div class="msg-meta">TIMMY // {{ timestamp }}</div>
<div class="msg-body">{{ response }}</div>
</div>
{% elif error %}
<div class="chat-message error-msg">
<div class="msg-meta">SYSTEM // {{ timestamp }}</div>
<div class="msg-body">{{ error }}</div>
</div>
{% endif %}

View File

@@ -0,0 +1,19 @@
<div class="panel-header">// SYSTEM HEALTH</div>
<div class="panel-body">
<div class="health-row">
<span class="health-label">OLLAMA</span>
{% if ollama %}
<span class="badge up">UP</span>
{% else %}
<span class="badge down">DOWN</span>
{% endif %}
</div>
<div class="health-row">
<span class="health-label">TIMMY</span>
<span class="badge ready">READY</span>
</div>
<div class="health-row">
<span class="health-label">MODEL</span>
<span class="badge ready">llama3.2</span>
</div>
</div>

0
src/timmy/__init__.py Normal file
View File

18
src/timmy/agent.py Normal file
View File

@@ -0,0 +1,18 @@
from agno.agent import Agent
from agno.models.ollama import Ollama
from agno.db.sqlite import SqliteDb
from timmy.prompts import TIMMY_SYSTEM_PROMPT
def create_timmy(db_file: str = "timmy.db") -> Agent:
"""Instantiate Timmy with Agno + Ollama + SQLite memory."""
return Agent(
name="Timmy",
model=Ollama(id="llama3.2"),
db=SqliteDb(db_file=db_file),
description=TIMMY_SYSTEM_PROMPT,
add_history_to_context=True,
num_history_runs=10,
markdown=True,
)

30
src/timmy/cli.py Normal file
View File

@@ -0,0 +1,30 @@
import typer
from timmy.agent import create_timmy
app = typer.Typer(help="Timmy — sovereign AI agent")
@app.command()
def think(topic: str = typer.Argument(..., help="Topic to reason about")):
"""Ask Timmy to think carefully about a topic."""
timmy = create_timmy()
timmy.print_response(f"Think carefully about: {topic}", stream=True)
@app.command()
def chat(message: str = typer.Argument(..., help="Message to send")):
"""Send a message to Timmy."""
timmy = create_timmy()
timmy.print_response(message, stream=True)
@app.command()
def status():
"""Print Timmy's operational status."""
timmy = create_timmy()
timmy.print_response("Brief status report — one sentence.", stream=False)
def main():
app()

7
src/timmy/prompts.py Normal file
View File

@@ -0,0 +1,7 @@
TIMMY_SYSTEM_PROMPT = """You are Timmy — a sovereign AI agent running locally.
No cloud dependencies. You think clearly, speak plainly, act with intention.
Grounded in Christian faith, powered by Bitcoin economics, committed to the
user's digital sovereignty."""
TIMMY_STATUS_PROMPT = """You are Timmy. Give a one-sentence status report confirming
you are operational and running locally."""

238
static/style.css Normal file
View File

@@ -0,0 +1,238 @@
:root {
--bg-deep: #060d14;
--bg-panel: #0c1824;
--bg-card: #0f2030;
--border: #1a3a55;
--border-glow: #1e4d72;
--text: #b8d0e8;
--text-dim: #4a7a9a;
--text-bright: #ddeeff;
--green: #00e87a;
--green-dim: #00704a;
--amber: #ffb800;
--amber-dim: #7a5800;
--red: #ff4455;
--red-dim: #7a1a22;
--blue: #00aaff;
--font: 'JetBrains Mono', 'Courier New', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg-deep);
color: var(--text);
font-family: var(--font);
font-size: 13px;
min-height: 100vh;
overflow-x: hidden;
}
/* ── Header ─────────────────────────────────────── */
.mc-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
background: var(--bg-panel);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
.mc-title {
font-size: 18px;
font-weight: 700;
color: var(--text-bright);
letter-spacing: 0.15em;
}
.mc-subtitle {
font-size: 11px;
color: var(--text-dim);
letter-spacing: 0.2em;
margin-left: 16px;
}
.mc-time {
font-size: 14px;
color: var(--blue);
letter-spacing: 0.1em;
}
/* ── Layout ──────────────────────────────────────── */
.mc-main {
display: grid;
grid-template-columns: 260px 1fr;
gap: 16px;
padding: 16px;
height: calc(100vh - 52px);
}
/* ── Panels ──────────────────────────────────────── */
.panel {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.panel-header {
padding: 8px 14px;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
font-size: 10px;
font-weight: 700;
color: var(--text-dim);
letter-spacing: 0.2em;
text-transform: uppercase;
}
.panel-body { padding: 14px; }
/* ── Sidebar ─────────────────────────────────────── */
.sidebar {
grid-column: 1;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
/* ── Agent Card ──────────────────────────────────── */
.agent-card {
border: 1px solid var(--border);
border-radius: 3px;
padding: 12px;
background: var(--bg-card);
}
.agent-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.green { background: var(--green); box-shadow: 0 0 6px var(--green); }
.status-dot.amber { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
.status-dot.red { background: var(--red); box-shadow: 0 0 6px var(--red); }
.agent-name {
font-size: 14px;
font-weight: 700;
color: var(--text-bright);
letter-spacing: 0.1em;
}
.agent-meta { font-size: 11px; line-height: 2; }
.meta-key { color: var(--text-dim); display: inline-block; width: 60px; }
.meta-val { color: var(--text); }
/* ── Health ──────────────────────────────────────── */
.health-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 0;
border-bottom: 1px solid var(--border);
font-size: 12px;
}
.health-row:last-child { border-bottom: none; }
.health-label { color: var(--text-dim); letter-spacing: 0.08em; }
.badge {
padding: 2px 8px;
border-radius: 2px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
}
.badge.up { background: var(--green-dim); color: var(--green); }
.badge.down { background: var(--red-dim); color: var(--red); }
.badge.ready { background: var(--amber-dim); color: var(--amber); }
/* ── Chat Panel ──────────────────────────────────── */
.chat-panel {
display: flex;
flex-direction: column;
grid-column: 2;
}
.chat-log {
flex: 1;
overflow-y: auto;
padding: 14px;
}
.chat-message { margin-bottom: 16px; }
.msg-meta {
font-size: 10px;
color: var(--text-dim);
margin-bottom: 4px;
letter-spacing: 0.12em;
}
.chat-message.user .msg-meta { color: var(--blue); }
.chat-message.agent .msg-meta { color: var(--green); }
.chat-message.error-msg .msg-meta { color: var(--red); }
.msg-body {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 3px;
padding: 10px 12px;
line-height: 1.65;
white-space: pre-wrap;
word-break: break-word;
}
.chat-message.user .msg-body { border-color: var(--border-glow); }
.chat-message.agent .msg-body { border-left: 3px solid var(--green); }
.chat-message.error-msg .msg-body { border-left: 3px solid var(--red); color: var(--red); }
/* ── Chat Input ──────────────────────────────────── */
.chat-input-bar {
padding: 12px 14px;
background: var(--bg-card);
border-top: 1px solid var(--border);
display: flex;
gap: 8px;
}
.chat-input-bar input {
flex: 1;
background: var(--bg-deep);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-bright);
font-family: var(--font);
font-size: 13px;
padding: 8px 12px;
outline: none;
}
.chat-input-bar input:focus {
border-color: var(--border-glow);
box-shadow: 0 0 0 1px var(--border-glow);
}
.chat-input-bar input::placeholder { color: var(--text-dim); }
.chat-input-bar button {
background: var(--border-glow);
border: none;
border-radius: 3px;
color: var(--text-bright);
font-family: var(--font);
font-size: 12px;
font-weight: 700;
padding: 8px 18px;
cursor: pointer;
letter-spacing: 0.12em;
transition: background 0.15s, color 0.15s;
}
.chat-input-bar button:hover { background: var(--blue); color: var(--bg-deep); }
/* ── HTMX Loading ────────────────────────────────── */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { display: inline-block; color: var(--amber); animation: blink 0.8s infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
/* ── Scrollbar ───────────────────────────────────── */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: var(--bg-deep); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-glow); }

0
tests/__init__.py Normal file
View File

25
tests/conftest.py Normal file
View File

@@ -0,0 +1,25 @@
import sys
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from fastapi.testclient import TestClient
# ── Mock agno so tests run without it installed ───────────────────────────────
# Uses setdefault: real module is used if installed, mock otherwise.
for _mod in [
"agno",
"agno.agent",
"agno.models",
"agno.models.ollama",
"agno.db",
"agno.db.sqlite",
]:
sys.modules.setdefault(_mod, MagicMock())
@pytest.fixture
def client():
from dashboard.app import app
with TestClient(app) as c:
yield c

79
tests/test_agent.py Normal file
View File

@@ -0,0 +1,79 @@
from unittest.mock import MagicMock, patch
def test_create_timmy_returns_agent():
"""create_timmy should delegate to Agno Agent with correct config."""
with patch("timmy.agent.Agent") as MockAgent, \
patch("timmy.agent.Ollama"), \
patch("timmy.agent.SqliteDb"):
mock_instance = MagicMock()
MockAgent.return_value = mock_instance
from timmy.agent import create_timmy
result = create_timmy()
assert result is mock_instance
MockAgent.assert_called_once()
def test_create_timmy_agent_name():
with patch("timmy.agent.Agent") as MockAgent, \
patch("timmy.agent.Ollama"), \
patch("timmy.agent.SqliteDb"):
from timmy.agent import create_timmy
create_timmy()
kwargs = MockAgent.call_args.kwargs
assert kwargs["name"] == "Timmy"
def test_create_timmy_uses_llama32():
with patch("timmy.agent.Agent"), \
patch("timmy.agent.Ollama") as MockOllama, \
patch("timmy.agent.SqliteDb"):
from timmy.agent import create_timmy
create_timmy()
MockOllama.assert_called_once_with(id="llama3.2")
def test_create_timmy_history_config():
with patch("timmy.agent.Agent") as MockAgent, \
patch("timmy.agent.Ollama"), \
patch("timmy.agent.SqliteDb"):
from timmy.agent import create_timmy
create_timmy()
kwargs = MockAgent.call_args.kwargs
assert kwargs["add_history_to_context"] is True
assert kwargs["num_history_runs"] == 10
assert kwargs["markdown"] is True
def test_create_timmy_custom_db_file():
with patch("timmy.agent.Agent"), \
patch("timmy.agent.Ollama"), \
patch("timmy.agent.SqliteDb") as MockDb:
from timmy.agent import create_timmy
create_timmy(db_file="custom.db")
MockDb.assert_called_once_with(db_file="custom.db")
def test_create_timmy_embeds_system_prompt():
from timmy.prompts import TIMMY_SYSTEM_PROMPT
with patch("timmy.agent.Agent") as MockAgent, \
patch("timmy.agent.Ollama"), \
patch("timmy.agent.SqliteDb"):
from timmy.agent import create_timmy
create_timmy()
kwargs = MockAgent.call_args.kwargs
assert kwargs["description"] == TIMMY_SYSTEM_PROMPT

110
tests/test_dashboard.py Normal file
View File

@@ -0,0 +1,110 @@
from unittest.mock import AsyncMock, MagicMock, patch
# ── Index ─────────────────────────────────────────────────────────────────────
def test_index_returns_200(client):
response = client.get("/")
assert response.status_code == 200
def test_index_contains_title(client):
response = client.get("/")
assert "TIMMY TIME" in response.text
def test_index_contains_chat_interface(client):
response = client.get("/")
assert "TIMMY INTERFACE" in response.text
# ── Health ────────────────────────────────────────────────────────────────────
def test_health_endpoint_ok(client):
with patch("dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=True):
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["services"]["ollama"] == "up"
assert "timmy" in data["agents"]
def test_health_endpoint_ollama_down(client):
with patch("dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=False):
response = client.get("/health")
assert response.status_code == 200
assert response.json()["services"]["ollama"] == "down"
def test_health_status_panel_ollama_up(client):
with patch("dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=True):
response = client.get("/health/status")
assert response.status_code == 200
assert "UP" in response.text
def test_health_status_panel_ollama_down(client):
with patch("dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=False):
response = client.get("/health/status")
assert response.status_code == 200
assert "DOWN" in response.text
# ── Agents ────────────────────────────────────────────────────────────────────
def test_agents_list(client):
response = client.get("/agents")
assert response.status_code == 200
data = response.json()
assert "agents" in data
ids = [a["id"] for a in data["agents"]]
assert "timmy" in ids
def test_agents_list_timmy_metadata(client):
response = client.get("/agents")
timmy = next(a for a in response.json()["agents"] if a["id"] == "timmy")
assert timmy["name"] == "Timmy"
assert timmy["model"] == "llama3.2"
assert timmy["type"] == "sovereign"
# ── Chat ──────────────────────────────────────────────────────────────────────
def test_chat_timmy_success(client):
mock_agent = MagicMock()
mock_run = MagicMock()
mock_run.content = "I am Timmy, operational and sovereign."
mock_agent.run.return_value = mock_run
with patch("dashboard.routes.agents.create_timmy", return_value=mock_agent):
response = client.post("/agents/timmy/chat", data={"message": "status?"})
assert response.status_code == 200
assert "status?" in response.text
assert "I am Timmy" in response.text
def test_chat_timmy_shows_user_message(client):
mock_agent = MagicMock()
mock_agent.run.return_value = MagicMock(content="Acknowledged.")
with patch("dashboard.routes.agents.create_timmy", return_value=mock_agent):
response = client.post("/agents/timmy/chat", data={"message": "hello there"})
assert "hello there" in response.text
def test_chat_timmy_ollama_offline(client):
with patch("dashboard.routes.agents.create_timmy", side_effect=Exception("connection refused")):
response = client.post("/agents/timmy/chat", data={"message": "ping"})
assert response.status_code == 200
assert "Timmy is offline" in response.text
assert "ping" in response.text
def test_chat_timmy_requires_message(client):
response = client.post("/agents/timmy/chat", data={})
assert response.status_code == 422

33
tests/test_prompts.py Normal file
View File

@@ -0,0 +1,33 @@
from timmy.prompts import TIMMY_SYSTEM_PROMPT, TIMMY_STATUS_PROMPT
def test_system_prompt_not_empty():
assert TIMMY_SYSTEM_PROMPT.strip()
def test_system_prompt_has_timmy_identity():
assert "Timmy" in TIMMY_SYSTEM_PROMPT
def test_system_prompt_mentions_sovereignty():
assert "sovereignty" in TIMMY_SYSTEM_PROMPT.lower()
def test_system_prompt_references_local():
assert "local" in TIMMY_SYSTEM_PROMPT.lower()
def test_system_prompt_is_multiline():
assert "\n" in TIMMY_SYSTEM_PROMPT
def test_status_prompt_not_empty():
assert TIMMY_STATUS_PROMPT.strip()
def test_status_prompt_has_timmy():
assert "Timmy" in TIMMY_STATUS_PROMPT
def test_prompts_are_distinct():
assert TIMMY_SYSTEM_PROMPT != TIMMY_STATUS_PROMPT