presentercoach / apps /api /tests /test_presenter_coach.py
nathycodes's picture
Deploy Tech Presenter Coach
ec17af3
Raw History Blame Contribute Delete
9.64 kB
import os
import tempfile
import time
from uuid import uuid4
test_id = uuid4().hex
os.environ["DATABASE_URL"] = f"sqlite:///./test_{test_id}.db"
os.environ["LOCAL_ONLY_MODE"] = "true"
os.environ["STORAGE_DIR"] = os.path.join(tempfile.gettempdir(), f"speech_machine_test_{test_id}")
from fastapi.testclient import TestClient
from app.db import Base, engine
from app.main import app
client = TestClient(app)
Base.metadata.create_all(bind=engine)
_headers: dict[str, str] | None = None
def get_headers() -> dict[str, str]:
global _headers
if _headers is not None:
return _headers
status = client.get("/setup/status").json()
if status["setup_required"]:
res = client.post("/setup", json={"email": "presenter@example.com", "password": "secret123"})
assert res.status_code == 201
else:
client.post("/auth/signup", json={"email": "presenter@example.com", "password": "secret123"})
login = client.post("/auth/login", json={"email": "presenter@example.com", "password": "secret123"})
assert login.status_code == 200
_headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
return _headers
def test_curriculum_profile_and_30_day_plan_auto_completion():
headers = get_headers()
profile_res = client.get("/curriculum/profile", headers=headers)
assert profile_res.status_code == 200
assert profile_res.json()["target_wpm_min"] == 125
update_res = client.put(
"/curriculum/profile",
json={
"display_name": "Ada Presenter",
"primary_role": "Staff Systems Engineer",
"focus_track": "keynote_clarity",
"target_wpm_min": 130,
"target_wpm_max": 155,
"daily_minutes_goal": 20,
"weaknesses": ["filler_words", "eye_contact", "rushing_pace"],
},
headers=headers,
)
assert update_res.status_code == 200
assert update_res.json()["display_name"] == "Ada Presenter"
assert update_res.json()["focus_track"] == "keynote_clarity"
plan_res = client.get("/curriculum/plan", headers=headers)
assert plan_res.status_code == 200
plan = plan_res.json()
assert plan["summary"]["total_days"] == 30
assert plan["summary"]["recommended_day"] == 1
assert len(plan["days"]) == 30
# Save a Day 1 session and verify Day 1 auto-completes and Day 2 becomes recommended
sess_res = client.post(
"/sessions",
json={
"title": "Day 1: Baseline 60-Second Tech Intro",
"mode": "Pitch",
"curriculum_day": 1,
"target_keywords": ["building", "technology", "problem", "matters"],
"duration_ms": 60000,
"eye_contact_pct": 0.55,
"transcript_segments": [
{
"start_ms": 0,
"end_ms": 30000,
"text": "Hi, um I am building a technology product because solving this problem really matters.",
},
{
"start_ms": 30000,
"end_ms": 60000,
"text": "First we measure speaking pace, um like second we track camera eye contact, therefore every rep improves.",
},
],
"metrics": [
{"t": 1, "wpm": 138, "rms": 55, "eye_contact": 0.55, "smile": 0.4, "yaw": 0.2, "pitch": 0.1, "roll": 0.1, "filler_count": 3, "pause_ms": 900}
],
},
headers=headers,
)
assert sess_res.status_code == 200
day1_session_id = sess_res.json()["id"]
assert sess_res.json()["curriculum_day"] == 1
assert sess_res.json()["transcription_status"] == "complete"
updated_plan = client.get("/curriculum/plan", headers=headers).json()
assert updated_plan["summary"]["completed_days"] >= 1
assert updated_plan["summary"]["recommended_day"] == 2
assert updated_plan["days"][0]["completed"] is True
assert updated_plan["days"][0]["completed_session_id"] == day1_session_id
note_res = client.post(
"/curriculum/days/1/complete",
json={"reflection_notes": "Felt much calmer speaking right to the lens."},
headers=headers,
)
assert note_res.status_code == 200
assert "calmer" in note_res.json()["reflection_notes"]
def test_targeted_exercises_and_tech_news_storyboard(monkeypatch):
headers = get_headers()
exercises_res = client.get("/curriculum/exercises", headers=headers)
assert exercises_res.status_code == 200
ex = exercises_res.json()
assert len(ex["daily_warmups"]) >= 4
assert len(ex["camera_challenges"]) >= 5
assert len(ex["targeted_exercises"]) >= 2
# Avoid network dependency in unit test for news feed
from app.services import tech_news
tech_news._FEED_CACHE["timestamp"] = time.time()
tech_news._FEED_CACHE["items"] = []
feed_res = client.get("/news/feed", headers=headers)
assert feed_res.status_code == 200
feed = feed_res.json()
assert len(feed["stories"]) >= 6
assert len(feed["frameworks"]) == 4
for fw in ("briefing_60s", "explainer_2m", "contrarian_take", "war_story"):
sb_res = client.post(
"/news/storyboard",
json={
"headline": "SQLite WAL Mode Scales Single-Node Web Apps",
"summary": "Modern NVMe disks let single-node SQLite handle tens of thousands of reads per second.",
"category": "Systems & Cloud",
"framework": fw,
"personal_angle": "Start simple before adopting distributed microservices.",
},
headers=headers,
)
assert sb_res.status_code == 200
sb = sb_res.json()
assert len(sb["sections"]) == 4
assert "SQLite" in sb["teleprompter_script"]
assert len(sb["target_keywords"]) >= 3
def test_auto_builtin_coaching_transcript_patch_weekly_review_and_compare():
headers = get_headers()
s1 = client.post(
"/sessions",
json={
"title": "Take 1 — Rushed & Filler-Heavy",
"mode": "Storytelling",
"duration_ms": 60000,
"eye_contact_pct": 0.45,
"transcript_segments": [
{
"start_ms": 0,
"end_ms": 60000,
"text": "Um like basically you know we built an API um like actually very fast um yeah.",
}
],
"metrics": [
{"t": 1, "wpm": 175, "rms": 45, "eye_contact": 0.45, "smile": 0.2, "yaw": 0.5, "pitch": 0.4, "roll": 0.2, "filler_count": 6, "pause_ms": 0}
],
},
headers=headers,
).json()
# Auto coaching generates immediately using built-in presenter coach
coach_res = client.get(f"/coaching/session/{s1['id']}?auto=true", headers=headers)
assert coach_res.status_code == 200
coach = coach_res.json()
assert coach["status"] == "completed"
assert len(coach["strengths"]) >= 2
assert len(coach["improvements"]) >= 2
assert len(coach["drills"]) >= 1
# Patch transcript with cleaner take and verify score + metrics recompute
s2 = client.post(
"/sessions",
json={
"title": "Take 2 — Crisp Keynote Structure",
"mode": "Pitch",
"duration_ms": 60000,
"eye_contact_pct": 0.82,
"transcript_segments": [],
"metrics": [
{"t": 1, "wpm": 142, "rms": 62, "eye_contact": 0.82, "smile": 0.55, "yaw": 0.1, "pitch": 0.1, "roll": 0.1, "filler_count": 0, "pause_ms": 850}
],
},
headers=headers,
).json()
patch_res = client.patch(
f"/sessions/{s2['id']}/transcript",
json={
"transcript_segments": [
{
"start_ms": 0,
"end_ms": 30000,
"text": "First, our engineering team simplified database queries because tail latency hurt mobile users.",
},
{
"start_ms": 30000,
"end_ms": 60000,
"text": "Second, response times dropped by seventy percent. Therefore, every page now loads under one second.",
},
]
},
headers=headers,
)
assert patch_res.status_code == 200
assert patch_res.json()["transcription_status"] == "complete"
assert patch_res.json()["score"]["total"] > s1["score"]["total"]
# Compare Take 1 vs Take 2
cmp_res = client.get(f"/dashboard/compare?session_a={s1['id']}&session_b={s2['id']}", headers=headers)
assert cmp_res.status_code == 200
cmp_data = cmp_res.json()
assert cmp_data["comparison"]["overall_score"]["delta"] > 0
assert len(cmp_data["insights"]) >= 1
assert cmp_data["session_b"]["title"] == "Take 2 — Crisp Keynote Structure"
# Weekly review + reflection note
weekly_res = client.get("/dashboard/weekly-review", headers=headers)
assert weekly_res.status_code == 200
weekly = weekly_res.json()
assert weekly["current_week"]["session_count"] >= 2
assert len(weekly["wins"]) >= 1
note_save = client.put(
"/dashboard/weekly-review/note",
json={
"week_start_date": weekly["week_start_date"],
"wins_note": "Cut fillers dramatically on Take 2!",
"focus_next_week": "Practice 2-minute Tech News explainers from bullet cues.",
},
headers=headers,
)
assert note_save.status_code == 200
assert "Cut fillers" in note_save.json()["wins_note"]