feat: wire mobile app to real Timmy backend via JSON REST API (#73)
Add /api/chat, /api/upload, and /api/chat/history endpoints to the FastAPI dashboard so the Expo mobile app talks directly to Timmy's brain (Ollama) instead of a non-existent Node.js server. Backend: - New src/dashboard/routes/chat_api.py with 4 endpoints - Mount /uploads/ for serving chat attachments - Same context injection and session management as HTMX chat Mobile app fixes: - Point API base URL at port 8000 (FastAPI) instead of 3000 - Create lib/_core/theme.ts (was referenced but never created) - Fix shared/types.ts (remove broken drizzle/errors re-exports) - Remove broken server/chat.ts and 1,235-line template README - Clean package.json (remove express, mysql2, drizzle, tRPC deps) - Remove debug console.log from theme-provider Tests: 13 new tests covering all API endpoints (all passing). https://claude.ai/code/session_01XqErDoh2rVsPY8oTj21Lz2 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
18ed6232f9
commit
5e60a6453b
177
tests/dashboard/test_chat_api.py
Normal file
177
tests/dashboard/test_chat_api.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""Tests for the JSON chat API (/api/chat, /api/upload, /api/chat/history)."""
|
||||
|
||||
import io
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
# ── POST /api/chat ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_api_chat_success(client):
|
||||
with patch("dashboard.routes.chat_api.timmy_chat", return_value="Hello from Timmy."):
|
||||
response = client.post(
|
||||
"/api/chat",
|
||||
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["reply"] == "Hello from Timmy."
|
||||
assert "timestamp" in data
|
||||
|
||||
|
||||
def test_api_chat_multimodal_content(client):
|
||||
"""Multimodal content arrays should extract text parts."""
|
||||
with patch("dashboard.routes.chat_api.timmy_chat", return_value="I see an image."):
|
||||
response = client.post(
|
||||
"/api/chat",
|
||||
json={
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What is this?"},
|
||||
{"type": "image_url", "image_url": {"url": "/uploads/test.jpg"}},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["reply"] == "I see an image."
|
||||
|
||||
|
||||
def test_api_chat_empty_messages(client):
|
||||
response = client.post("/api/chat", json={"messages": []})
|
||||
assert response.status_code == 400
|
||||
assert "error" in response.json()
|
||||
|
||||
|
||||
def test_api_chat_missing_messages(client):
|
||||
response = client.post("/api/chat", json={"foo": "bar"})
|
||||
assert response.status_code == 400
|
||||
assert "messages" in response.json()["error"]
|
||||
|
||||
|
||||
def test_api_chat_no_user_message(client):
|
||||
response = client.post(
|
||||
"/api/chat",
|
||||
json={"messages": [{"role": "assistant", "content": "hi"}]},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "No user message" in response.json()["error"]
|
||||
|
||||
|
||||
def test_api_chat_ollama_offline(client):
|
||||
with patch(
|
||||
"dashboard.routes.chat_api.timmy_chat",
|
||||
side_effect=ConnectionError("Ollama unreachable"),
|
||||
):
|
||||
response = client.post(
|
||||
"/api/chat",
|
||||
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
data = response.json()
|
||||
assert "offline" in data["error"].lower() or "unreachable" in data["error"].lower()
|
||||
|
||||
|
||||
def test_api_chat_logs_to_message_log(client):
|
||||
from dashboard.store import message_log
|
||||
|
||||
with patch("dashboard.routes.chat_api.timmy_chat", return_value="Reply."):
|
||||
client.post(
|
||||
"/api/chat",
|
||||
json={"messages": [{"role": "user", "content": "test msg"}]},
|
||||
)
|
||||
|
||||
entries = message_log.all()
|
||||
assert len(entries) == 2
|
||||
assert entries[0].role == "user"
|
||||
assert entries[0].content == "test msg"
|
||||
assert entries[1].role == "agent"
|
||||
assert entries[1].content == "Reply."
|
||||
|
||||
|
||||
def test_api_chat_invalid_json(client):
|
||||
response = client.post(
|
||||
"/api/chat",
|
||||
content=b"not json",
|
||||
headers={"content-type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# ── POST /api/upload ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_api_upload_file(client, tmp_path):
|
||||
with patch("dashboard.routes.chat_api._UPLOAD_DIR", str(tmp_path)):
|
||||
response = client.post(
|
||||
"/api/upload",
|
||||
files={"file": ("test.txt", io.BytesIO(b"hello world"), "text/plain")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["fileName"] == "test.txt"
|
||||
assert data["mimeType"] == "text/plain"
|
||||
assert "/uploads/" in data["url"]
|
||||
|
||||
|
||||
def test_api_upload_image(client, tmp_path):
|
||||
# 1x1 red PNG
|
||||
png = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
|
||||
b"\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00"
|
||||
)
|
||||
with patch("dashboard.routes.chat_api._UPLOAD_DIR", str(tmp_path)):
|
||||
response = client.post(
|
||||
"/api/upload",
|
||||
files={"file": ("photo.png", io.BytesIO(png), "image/png")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["fileName"] == "photo.png"
|
||||
assert data["mimeType"] == "image/png"
|
||||
|
||||
|
||||
# ── GET /api/chat/history ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_api_chat_history_empty(client):
|
||||
response = client.get("/api/chat/history")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["messages"] == []
|
||||
|
||||
|
||||
def test_api_chat_history_after_chat(client):
|
||||
with patch("dashboard.routes.chat_api.timmy_chat", return_value="Hi!"):
|
||||
client.post(
|
||||
"/api/chat",
|
||||
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||
)
|
||||
|
||||
response = client.get("/api/chat/history")
|
||||
assert response.status_code == 200
|
||||
msgs = response.json()["messages"]
|
||||
assert len(msgs) == 2
|
||||
assert msgs[0]["role"] == "user"
|
||||
assert msgs[1]["role"] == "agent"
|
||||
|
||||
|
||||
# ── DELETE /api/chat/history ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_api_clear_history(client):
|
||||
from dashboard.store import message_log
|
||||
|
||||
message_log.append(role="user", content="old", timestamp="00:00:00")
|
||||
|
||||
response = client.delete("/api/chat/history")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
assert len(message_log) == 0
|
||||
Reference in New Issue
Block a user