Spaces:
Paused
Paused
Download apps/api/tests/test_api.py from nathycodes/presentercoach: direct link, hf CLI and curl.
- Browser
- Download file 29 kB
-
https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/api/tests/test_api.py
- Command line
-
hf download hf://spaces/nathycodes/presentercoach/apps/api/tests/test_api.py
-
curl -L -o test_api.py https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/api/tests/test_api.py
29 kB
| import json | |
| import os | |
| import tempfile | |
| import zipfile | |
| import asyncio | |
| from io import BytesIO | |
| 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 sqlalchemy import create_engine, inspect | |
| from app import models, schema_upgrades | |
| from app.db import Base, SessionLocal, engine | |
| from app.main import app | |
| from app.routes import settings as settings_routes | |
| from app.routes import coaching as coaching_routes | |
| from app.services.coaching import build_coaching_prompt, parse_coaching_response | |
| from app.worker import jobs as worker_jobs | |
| client = TestClient(app) | |
| Base.metadata.create_all(bind=engine) | |
| _admin_headers: dict[str, str] | None = None | |
| def get_admin_headers() -> dict[str, str]: | |
| global _admin_headers | |
| if _admin_headers is not None: | |
| return _admin_headers | |
| status_response = client.get("/setup/status") | |
| if status_response.json()["setup_required"]: | |
| setup_response = client.post( | |
| "/setup", | |
| json={"email": "admin@example.com", "password": "secret123"}, | |
| ) | |
| assert setup_response.status_code == 201 | |
| response = client.post("/auth/login", json={"email": "admin@example.com", "password": "secret123"}) | |
| assert response.status_code == 200 | |
| _admin_headers = {"Authorization": f"Bearer {response.json()['access_token']}"} | |
| return _admin_headers | |
| def create_transcription_model(model_id: str, name: str, available: bool = True, local_path: str | None = None): | |
| return { | |
| "id": model_id, | |
| "name": name, | |
| "repo_id": model_id, | |
| "description": f"{name} model", | |
| "languages": "Multiple languages", | |
| "available": available, | |
| "local_path": local_path, | |
| } | |
| def test_auth_flow(): | |
| response = client.get("/setup/status") | |
| if response.json()["setup_required"]: | |
| response = client.post("/setup", json={"email": "admin@example.com", "password": "secret123"}) | |
| assert response.status_code == 201 | |
| response = client.post("/auth/signup", json={"email": "test@example.com", "password": "secret123"}) | |
| assert response.status_code == 200 | |
| token = response.json()["access_token"] | |
| assert token | |
| response = client.post("/auth/login", json={"email": "test@example.com", "password": "secret123"}) | |
| assert response.status_code == 200 | |
| def test_refresh_rotation_and_logout(): | |
| status_response = client.get("/setup/status") | |
| if status_response.json()["setup_required"]: | |
| client.post("/setup", json={"email": "admin@example.com", "password": "secret123"}) | |
| login_response = client.post( | |
| "/auth/login", | |
| json={"email": "admin@example.com", "password": "secret123"}, | |
| ) | |
| assert login_response.status_code == 200 | |
| first_refresh_cookie = login_response.cookies.get("refresh_token") | |
| refresh_response = client.post("/auth/refresh", cookies={"refresh_token": first_refresh_cookie}) | |
| assert refresh_response.status_code == 200 | |
| assert refresh_response.cookies.get("refresh_token") | |
| assert refresh_response.cookies.get("refresh_token") != first_refresh_cookie | |
| reused_response = client.post("/auth/refresh", cookies={"refresh_token": first_refresh_cookie}) | |
| assert reused_response.status_code == 401 | |
| logout_response = client.post("/auth/logout", cookies={"refresh_token": refresh_response.cookies.get("refresh_token")}) | |
| assert logout_response.status_code == 200 | |
| after_logout = client.post("/auth/refresh", cookies={"refresh_token": refresh_response.cookies.get("refresh_token")}) | |
| assert after_logout.status_code == 401 | |
| def test_health_endpoints(): | |
| assert client.get("/health").status_code == 200 | |
| response = client.get("/health/ready") | |
| assert response.status_code == 200 | |
| assert "database" in response.json()["checks"] | |
| def test_transcription_settings_endpoint(monkeypatch): | |
| monkeypatch.setattr(settings_routes.transcription, "get_default_transcription_backend", lambda: "mlx_audio") | |
| monkeypatch.setattr( | |
| settings_routes.transcription, | |
| "get_transcription_backends", | |
| lambda: [ | |
| { | |
| "id": "server", | |
| "label": "Built-in Server", | |
| "supported": True, | |
| "live": True, | |
| "detail": "Server path", | |
| "models": [ | |
| create_transcription_model("openai/whisper-small", "Whisper Small", available=False), | |
| create_transcription_model("openai/whisper-medium", "Whisper Medium", available=True), | |
| ], | |
| "default_model": "openai/whisper-small", | |
| }, | |
| { | |
| "id": "mlx_audio", | |
| "label": "MLX Audio", | |
| "supported": True, | |
| "live": True, | |
| "detail": "Apple Silicon local ASR.", | |
| "models": [ | |
| create_transcription_model( | |
| "mlx-community/parakeet-tdt-0.6b-v3", | |
| "Parakeet", | |
| local_path="/tmp/models/parakeet", | |
| ) | |
| ], | |
| "default_model": "mlx-community/parakeet-tdt-0.6b-v3", | |
| }, | |
| ], | |
| ) | |
| monkeypatch.setattr( | |
| settings_routes, | |
| "get_all_engine_statuses", | |
| lambda: [ | |
| { | |
| "id": "ollama", | |
| "label": "Ollama", | |
| "base_url": "http://127.0.0.1:11434", | |
| "live": True, | |
| "detail": "Connected", | |
| "available_models": ["qwen3-asr"], | |
| } | |
| ], | |
| ) | |
| headers = get_admin_headers() | |
| response = client.get("/settings/transcription", headers=headers) | |
| assert response.status_code == 200 | |
| assert response.json()["default_backend"] == "mlx_audio" | |
| assert response.json()["backends"][1]["id"] == "mlx_audio" | |
| assert response.json()["backends"][1]["models"][0]["id"] == "mlx-community/parakeet-tdt-0.6b-v3" | |
| assert response.json()["backends"][1]["models"][0]["available"] is True | |
| assert response.json()["text_inference_engines"][0]["id"] == "ollama" | |
| def test_transcription_settings_endpoint_marks_unsupported_mlx_audio(monkeypatch): | |
| monkeypatch.setattr(settings_routes.transcription, "get_default_transcription_backend", lambda: "server") | |
| monkeypatch.setattr( | |
| settings_routes.transcription, | |
| "get_transcription_backends", | |
| lambda: [ | |
| { | |
| "id": "server", | |
| "label": "Built-in Server", | |
| "supported": True, | |
| "live": True, | |
| "detail": "Server path", | |
| "models": [create_transcription_model("openai/whisper-small", "Whisper Small", available=False)], | |
| "default_model": "openai/whisper-small", | |
| }, | |
| { | |
| "id": "mlx_audio", | |
| "label": "MLX Audio", | |
| "supported": False, | |
| "live": False, | |
| "detail": "MLX Audio requires macOS on Apple Silicon.", | |
| "models": [create_transcription_model("mlx-community/parakeet-tdt-0.6b-v3", "Parakeet", available=False)], | |
| "default_model": None, | |
| }, | |
| ], | |
| ) | |
| monkeypatch.setattr(settings_routes, "get_all_engine_statuses", lambda: []) | |
| headers = get_admin_headers() | |
| response = client.get("/settings/transcription", headers=headers) | |
| assert response.status_code == 200 | |
| assert response.json()["default_backend"] == "server" | |
| assert response.json()["backends"][1]["supported"] is False | |
| assert response.json()["backends"][1]["models"][0]["available"] is False | |
| def test_transcription_model_download_endpoint(monkeypatch): | |
| monkeypatch.setattr( | |
| settings_routes.transcription, | |
| "download_transcription_model", | |
| lambda backend_id, model_id: create_transcription_model( | |
| model_id, | |
| "Parakeet", | |
| available=True, | |
| local_path="/tmp/models/parakeet", | |
| ), | |
| ) | |
| headers = get_admin_headers() | |
| response = client.post( | |
| "/settings/transcription/models/download", | |
| json={"backend": "mlx_audio", "model": "mlx-community/parakeet-tdt-0.6b-v3"}, | |
| headers=headers, | |
| ) | |
| assert response.status_code == 200 | |
| assert response.json()["id"] == "mlx-community/parakeet-tdt-0.6b-v3" | |
| assert response.json()["available"] is True | |
| assert response.json()["local_path"] == "/tmp/models/parakeet" | |
| def test_local_inference_check_endpoint(monkeypatch): | |
| monkeypatch.setattr( | |
| settings_routes, | |
| "check_engine", | |
| lambda engine_id, base_url=None: { | |
| "id": engine_id, | |
| "label": "LM Studio", | |
| "base_url": base_url or "http://127.0.0.1:1234/v1", | |
| "live": True, | |
| "detail": "Connected", | |
| "available_models": ["mistralai/Voxtral-Mini-4B-Realtime-2602"], | |
| }, | |
| ) | |
| headers = get_admin_headers() | |
| response = client.post( | |
| "/settings/local-inference/check", | |
| json={"engine_id": "lmstudio", "base_url": "http://localhost:1234/v1"}, | |
| headers=headers, | |
| ) | |
| assert response.status_code == 200 | |
| assert response.json()["live"] is True | |
| assert response.json()["available_models"] == ["mistralai/Voxtral-Mini-4B-Realtime-2602"] | |
| def test_session_create_and_upload(): | |
| headers = get_admin_headers() | |
| payload = { | |
| "title": "Test", | |
| "mode": "Interview", | |
| "duration_ms": 1000, | |
| "wpm_avg": 120, | |
| "filler_count": 0, | |
| "eye_contact_pct": 0.8, | |
| "tags": ["demo"], | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 1000, "text": "Hello"}], | |
| "metrics": [{"t": 0, "wpm": 120, "rms": 50, "eye_contact": 0.8, "smile": 0.3, "yaw": 0.1, "pitch": 0.2, "roll": 0.1, "filler_count": 0, "pause_ms": 800}], | |
| } | |
| response = client.post("/sessions", json=payload, headers=headers) | |
| assert response.status_code == 200 | |
| session_id = response.json()["id"] | |
| video_data = BytesIO(b"\x1a\x45\xdf\xa3webm-data") | |
| files = {"file": ("test.webm", video_data, "video/webm")} | |
| response = client.post(f"/sessions/{session_id}/upload", files=files, headers=headers) | |
| assert response.status_code == 200 | |
| assert response.json()["status"] == "complete" | |
| response = client.get("/sessions", headers=headers) | |
| assert response.status_code == 200 | |
| assert len(response.json()) >= 1 | |
| def test_export_includes_recording_file(): | |
| headers = get_admin_headers() | |
| payload = { | |
| "title": "Export", | |
| "mode": "Interview", | |
| "duration_ms": 1000, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 1000, "text": "Hello"}], | |
| "metrics": [], | |
| } | |
| response = client.post("/sessions", json=payload, headers=headers) | |
| session_id = response.json()["id"] | |
| files = {"file": ("test.webm", BytesIO(b"\x1a\x45\xdf\xa3webm-data"), "video/webm")} | |
| upload_response = client.post(f"/sessions/{session_id}/upload", files=files, headers=headers) | |
| assert upload_response.status_code == 200 | |
| export_response = client.get(f"/sessions/{session_id}/export", headers=headers) | |
| assert export_response.status_code == 200 | |
| with zipfile.ZipFile(BytesIO(export_response.content)) as archive: | |
| assert "recording.webm" in archive.namelist() | |
| assert "transcript.txt" in archive.namelist() | |
| assert "transcript.pdf" in archive.namelist() | |
| def test_transcript_download_formats(): | |
| headers = get_admin_headers() | |
| payload = { | |
| "title": "Transcript Download", | |
| "mode": "Upload", | |
| "duration_ms": 61000, | |
| "transcript_segments": [ | |
| {"start_ms": 0, "end_ms": 1000, "text": "Hello world"}, | |
| {"start_ms": 1000, "end_ms": 2000, "text": "This is a transcript"}, | |
| ], | |
| "metrics": [], | |
| } | |
| response = client.post("/sessions", json=payload, headers=headers) | |
| session_id = response.json()["id"] | |
| db_response = client.get(f"/sessions/{session_id}", headers=headers) | |
| assert db_response.status_code == 200 | |
| txt_response = client.get(f"/sessions/{session_id}/transcript.txt", headers=headers) | |
| assert txt_response.status_code == 200 | |
| assert txt_response.headers["content-type"].startswith("text/plain") | |
| assert "Hello world" in txt_response.text | |
| pdf_response = client.get(f"/sessions/{session_id}/transcript.pdf", headers=headers) | |
| assert pdf_response.status_code == 200 | |
| assert pdf_response.headers["content-type"] == "application/pdf" | |
| assert pdf_response.content.startswith(b"%PDF") | |
| def test_delete_video_keeps_session_transcript(): | |
| headers = get_admin_headers() | |
| payload = { | |
| "title": "Delete Video", | |
| "mode": "Upload", | |
| "duration_ms": 5000, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 1000, "text": "Keep transcript"}], | |
| "metrics": [], | |
| } | |
| response = client.post("/sessions", json=payload, headers=headers) | |
| session_id = response.json()["id"] | |
| files = {"file": ("test.webm", BytesIO(b"\x1a\x45\xdf\xa3webm-data"), "video/webm")} | |
| upload_response = client.post(f"/sessions/{session_id}/upload", files=files, headers=headers) | |
| assert upload_response.status_code == 200 | |
| delete_response = client.delete(f"/sessions/{session_id}/video", headers=headers) | |
| assert delete_response.status_code == 200 | |
| assert delete_response.json()["status"] == "deleted" | |
| assert delete_response.json()["transcription_status"] == "complete" | |
| session_response = client.get(f"/sessions/{session_id}", headers=headers) | |
| assert session_response.status_code == 200 | |
| assert session_response.json()["video_path"] is None | |
| video_response = client.get(f"/sessions/{session_id}/video", headers=headers) | |
| assert video_response.status_code == 404 | |
| txt_response = client.get(f"/sessions/{session_id}/transcript.txt", headers=headers) | |
| assert txt_response.status_code == 200 | |
| assert "Keep transcript" in txt_response.text | |
| def test_video_upload_rejects_invalid_magic_bytes(): | |
| headers = get_admin_headers() | |
| payload = { | |
| "title": "Invalid Upload", | |
| "mode": "Upload", | |
| "duration_ms": 1000, | |
| "transcript_segments": [], | |
| "metrics": [], | |
| } | |
| response = client.post("/sessions", json=payload, headers=headers) | |
| session_id = response.json()["id"] | |
| upload_response = client.post( | |
| f"/sessions/{session_id}/upload", | |
| files={"file": ("bad.webm", BytesIO(b"not-a-webm"), "video/webm")}, | |
| headers=headers, | |
| ) | |
| assert upload_response.status_code == 415 | |
| def test_session_document_upload_round_trip_and_full_delete(): | |
| headers = get_admin_headers() | |
| payload = { | |
| "title": "Document Session", | |
| "mode": "Interview", | |
| "duration_ms": 5000, | |
| "transcript_segments": [], | |
| "metrics": [], | |
| } | |
| response = client.post("/sessions", json=payload, headers=headers) | |
| session_id = response.json()["id"] | |
| document_blocks = [{"index": 0, "text": "Read this paragraph aloud."}] | |
| upload_response = client.post( | |
| f"/sessions/{session_id}/document", | |
| files={"file": ("script.txt", BytesIO(b"Read this paragraph aloud."), "text/plain")}, | |
| data={"blocks_json": json.dumps(document_blocks)}, | |
| headers=headers, | |
| ) | |
| assert upload_response.status_code == 200 | |
| assert upload_response.json()["name"] == "script.txt" | |
| assert upload_response.json()["mime_type"] == "text/plain" | |
| session_response = client.get(f"/sessions/{session_id}", headers=headers) | |
| assert session_response.status_code == 200 | |
| assert session_response.json()["document"]["name"] == "script.txt" | |
| assert session_response.json()["document"]["blocks"][0]["text"] == "Read this paragraph aloud." | |
| with SessionLocal() as db: | |
| stored = db.query(models.Session).filter(models.Session.id == session_id).first() | |
| assert stored is not None | |
| assert stored.document_path is not None | |
| document_path = stored.document_path | |
| assert os.path.exists(document_path) | |
| delete_response = client.delete(f"/sessions/{session_id}", headers=headers) | |
| assert delete_response.status_code == 200 | |
| assert delete_response.json()["status"] == "deleted" | |
| assert not os.path.exists(document_path) | |
| missing_response = client.get(f"/sessions/{session_id}", headers=headers) | |
| assert missing_response.status_code == 404 | |
| def test_document_upload_rejects_unsupported_types(): | |
| headers = get_admin_headers() | |
| payload = { | |
| "title": "Unsupported Doc", | |
| "mode": "Interview", | |
| "duration_ms": 1000, | |
| "transcript_segments": [], | |
| "metrics": [], | |
| } | |
| response = client.post("/sessions", json=payload, headers=headers) | |
| session_id = response.json()["id"] | |
| upload_response = client.post( | |
| f"/sessions/{session_id}/document", | |
| files={ | |
| "file": ( | |
| "script.docx", | |
| BytesIO(b"fake-docx"), | |
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| ) | |
| }, | |
| data={"blocks_json": json.dumps([{"index": 0, "text": "Unsupported"}])}, | |
| headers=headers, | |
| ) | |
| assert upload_response.status_code == 400 | |
| assert "Unsupported document type" in upload_response.json()["detail"] | |
| def test_dashboard_endpoints_populate_metrics_and_aggregates(): | |
| headers = get_admin_headers() | |
| first = client.post( | |
| "/sessions", | |
| json={ | |
| "title": "Analytics One", | |
| "mode": "Practice", | |
| "duration_ms": 60000, | |
| "wpm_avg": 120, | |
| "filler_count": 2, | |
| "eye_contact_pct": 0.75, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 60000, "text": "Um hello world like today"}], | |
| "metrics": [ | |
| {"t": 0, "wpm": 120, "rms": 50, "eye_contact": 0.75, "smile": 0.4, "yaw": 0.1, "pitch": 0.2, "roll": 0.1, "filler_count": 1, "pause_ms": 900} | |
| ], | |
| }, | |
| headers=headers, | |
| ) | |
| assert first.status_code == 200 | |
| second = client.post( | |
| "/sessions", | |
| json={ | |
| "title": "Analytics Two", | |
| "mode": "Practice", | |
| "duration_ms": 60000, | |
| "wpm_avg": 140, | |
| "filler_count": 0, | |
| "eye_contact_pct": 0.8, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 60000, "text": "First we improve outcomes"}], | |
| "metrics": [ | |
| {"t": 0, "wpm": 140, "rms": 60, "eye_contact": 0.8, "smile": 0.5, "yaw": 0.1, "pitch": 0.1, "roll": 0.1, "filler_count": 0, "pause_ms": 700} | |
| ], | |
| }, | |
| headers=headers, | |
| ) | |
| assert second.status_code == 200 | |
| overview = client.get("/dashboard/overview", headers=headers) | |
| assert overview.status_code == 200 | |
| assert overview.json()["total_sessions"] >= 2 | |
| assert overview.json()["recent_30d"]["avg_overall_score"] >= 0 | |
| trends = client.get("/dashboard/trends/avg_overall_score", headers=headers) | |
| assert trends.status_code == 200 | |
| assert len(trends.json()) >= 1 | |
| sessions_response = client.get("/dashboard/sessions", headers=headers) | |
| assert sessions_response.status_code == 200 | |
| assert any(item["session_id"] == second.json()["id"] for item in sessions_response.json()) | |
| heatmap = client.get("/dashboard/heatmap", headers=headers) | |
| assert heatmap.status_code == 200 | |
| assert heatmap.json()[-1]["count"] >= 1 | |
| fillers = client.get("/dashboard/fillers", headers=headers) | |
| assert fillers.status_code == 200 | |
| assert fillers.json()["um"] >= 1 | |
| compare = client.get( | |
| f"/dashboard/compare?session_a={first.json()['id']}&session_b={second.json()['id']}", | |
| headers=headers, | |
| ) | |
| assert compare.status_code == 200 | |
| assert "overall_score" in compare.json()["comparison"] | |
| def test_dashboard_is_user_scoped(): | |
| headers = get_admin_headers() | |
| signup = client.post("/auth/signup", json={"email": "scoped@example.com", "password": "secret123"}) | |
| assert signup.status_code == 200 | |
| other_headers = {"Authorization": f"Bearer {signup.json()['access_token']}"} | |
| response = client.post( | |
| "/sessions", | |
| json={ | |
| "title": "Private", | |
| "mode": "Practice", | |
| "duration_ms": 1000, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 1000, "text": "Private session"}], | |
| "metrics": [], | |
| }, | |
| headers=headers, | |
| ) | |
| assert response.status_code == 200 | |
| other_sessions = client.get("/dashboard/sessions", headers=other_headers) | |
| assert other_sessions.status_code == 200 | |
| assert all(item["session_id"] != response.json()["id"] for item in other_sessions.json()) | |
| def test_goal_crud_progress_and_user_scoping(): | |
| headers = get_admin_headers() | |
| session_response = client.post( | |
| "/sessions", | |
| json={ | |
| "title": "Goal baseline", | |
| "mode": "Practice", | |
| "duration_ms": 60000, | |
| "wpm_avg": 130, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 60000, "text": "A focused baseline session"}], | |
| "metrics": [], | |
| }, | |
| headers=headers, | |
| ) | |
| assert session_response.status_code == 200 | |
| assert client.get("/dashboard/overview", headers=headers).status_code == 200 | |
| create_response = client.post( | |
| "/dashboard/goals", | |
| json={ | |
| "metric_name": "wpm", | |
| "comparison": "between", | |
| "target_value": 100, | |
| "target_upper": 160, | |
| "is_active": True, | |
| }, | |
| headers=headers, | |
| ) | |
| assert create_response.status_code == 201 | |
| goal = create_response.json() | |
| assert goal["metric_name"] == "wpm" | |
| assert goal["current_value"] is not None | |
| assert goal["achieved"] is True | |
| duplicate = client.post( | |
| "/dashboard/goals", | |
| json={"metric_name": "wpm", "comparison": "gte", "target_value": 120}, | |
| headers=headers, | |
| ) | |
| assert duplicate.status_code == 409 | |
| invalid_range = client.post( | |
| "/dashboard/goals", | |
| json={ | |
| "metric_name": "overall_score", | |
| "comparison": "between", | |
| "target_value": 80, | |
| "target_upper": 70, | |
| }, | |
| headers=headers, | |
| ) | |
| assert invalid_range.status_code == 422 | |
| update_response = client.patch( | |
| f"/dashboard/goals/{goal['id']}", | |
| json={"comparison": "gte", "target_value": 200, "is_active": False}, | |
| headers=headers, | |
| ) | |
| assert update_response.status_code == 200 | |
| assert update_response.json()["target_upper"] is None | |
| assert update_response.json()["achieved"] is False | |
| assert update_response.json()["is_active"] is False | |
| signup = client.post("/auth/signup", json={"email": "goal-scoped@example.com", "password": "secret123"}) | |
| assert signup.status_code == 200 | |
| other_headers = {"Authorization": f"Bearer {signup.json()['access_token']}"} | |
| assert client.patch(f"/dashboard/goals/{goal['id']}", json={"is_active": True}, headers=other_headers).status_code == 404 | |
| assert client.delete(f"/dashboard/goals/{goal['id']}", headers=other_headers).status_code == 404 | |
| assert client.get("/dashboard/goals", headers=other_headers).json() == [] | |
| delete_response = client.delete(f"/dashboard/goals/{goal['id']}", headers=headers) | |
| assert delete_response.status_code == 200 | |
| assert delete_response.json()["status"] == "deleted" | |
| def test_coaching_prompt_truncation_and_json_parsing(): | |
| long_transcript = "word " * 5000 | |
| prompt = build_coaching_prompt( | |
| long_transcript, | |
| {"wpm": 120, "overall_score": 70, "filler_breakdown": {"um": 2}}, | |
| [], | |
| active_goals=[ | |
| { | |
| "metric_name": "filler_rate", | |
| "comparison": "lte", | |
| "target_value": 1, | |
| "current_value": 2, | |
| "achieved": False, | |
| } | |
| ], | |
| ) | |
| assert "filler_rate: target lte 1" in prompt | |
| assert "[transcript truncated]" in prompt | |
| parsed = parse_coaching_response( | |
| json.dumps( | |
| { | |
| "summary": "Good session.", | |
| "strengths": [{"area": "pace", "detail": "Steady"}], | |
| "improvements": [{"area": "fillers", "detail": "Pause more", "priority": "medium"}], | |
| "drills": [{"name": "Pause drill", "description": "Pause", "duration_minutes": 5}], | |
| "practice_plan": {"focus_areas": ["fillers"], "next_session_goal": "Use fewer fillers"}, | |
| } | |
| ) | |
| ) | |
| assert parsed["summary"] == "Good session." | |
| def test_coaching_worker_success_and_failure(monkeypatch): | |
| headers = get_admin_headers() | |
| response = client.post( | |
| "/sessions", | |
| json={ | |
| "title": "Coach Me", | |
| "mode": "Practice", | |
| "duration_ms": 60000, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 60000, "text": "Hello coach"}], | |
| "metrics": [], | |
| }, | |
| headers=headers, | |
| ) | |
| session_id = response.json()["id"] | |
| def fake_generate(transcript, metrics, history, previous_feedback=None, active_goals=None): | |
| return { | |
| "summary": "Local feedback ready.", | |
| "strengths": [{"area": "clarity", "detail": "Clear opening"}], | |
| "improvements": [{"area": "pace", "detail": "Keep it steady", "priority": "low"}], | |
| "drills": [{"name": "Pace drill", "description": "Read for one minute"}], | |
| "practice_plan": {"focus_areas": ["pace"], "next_session_goal": "Stay near 140 WPM"}, | |
| "raw_response": "{}", | |
| "model_used": "llama3.2:3b", | |
| "generation_time_ms": 10, | |
| } | |
| monkeypatch.setattr(worker_jobs, "generate_coaching_feedback", fake_generate) | |
| result = asyncio.run(worker_jobs.generate_session_coaching({}, session_id, 1)) | |
| assert result["status"] == "completed" | |
| coaching_response = client.get(f"/coaching/session/{session_id}", headers=headers) | |
| assert coaching_response.status_code == 200 | |
| assert coaching_response.json()["summary"] == "Local feedback ready." | |
| def fake_failure(transcript, metrics, history, previous_feedback=None, active_goals=None): | |
| raise RuntimeError("Ollama unavailable") | |
| monkeypatch.setattr(worker_jobs, "generate_coaching_feedback", fake_failure) | |
| failed = asyncio.run(worker_jobs.generate_session_coaching({}, session_id, 1)) | |
| assert failed["status"] == "failed" | |
| failed_response = client.get(f"/coaching/session/{session_id}", headers=headers) | |
| assert failed_response.json()["status"] == "failed" | |
| assert "Ollama unavailable" in failed_response.json()["error_message"] | |
| def test_coaching_retry_endpoint_queues_job(monkeypatch): | |
| headers = get_admin_headers() | |
| response = client.post( | |
| "/sessions", | |
| json={ | |
| "title": "Retry Coach", | |
| "mode": "Practice", | |
| "duration_ms": 1000, | |
| "transcript_segments": [{"start_ms": 0, "end_ms": 1000, "text": "Retry coaching"}], | |
| "metrics": [], | |
| }, | |
| headers=headers, | |
| ) | |
| session_id = response.json()["id"] | |
| async def fake_enqueue(session_id_arg, user_id_arg): | |
| assert session_id_arg == session_id | |
| assert user_id_arg == 1 | |
| return "job-coaching-1" | |
| monkeypatch.setattr(coaching_routes, "enqueue_coaching", fake_enqueue) | |
| retry = client.post(f"/coaching/session/{session_id}/retry", headers=headers) | |
| assert retry.status_code == 200 | |
| assert retry.json() == {"status": "queued", "job_id": "job-coaching-1"} | |
| def test_schema_upgrade_adds_document_columns(tmp_path, monkeypatch): | |
| engine = create_engine(f"sqlite:///{tmp_path / 'legacy.db'}") | |
| with engine.begin() as connection: | |
| connection.exec_driver_sql( | |
| """ | |
| CREATE TABLE sessions ( | |
| id INTEGER PRIMARY KEY, | |
| user_id INTEGER NOT NULL, | |
| title VARCHAR(255), | |
| mode VARCHAR(80), | |
| prompt TEXT, | |
| goal VARCHAR(255), | |
| started_at DATETIME, | |
| ended_at DATETIME, | |
| duration_ms INTEGER, | |
| wpm_avg FLOAT, | |
| filler_count INTEGER, | |
| eye_contact_pct FLOAT, | |
| video_path VARCHAR(1024), | |
| transcription_status VARCHAR(32), | |
| created_at DATETIME | |
| ) | |
| """ | |
| ) | |
| monkeypatch.setattr(schema_upgrades, "engine", engine) | |
| schema_upgrades.apply_startup_schema_upgrades() | |
| columns = {column["name"] for column in inspect(engine).get_columns("sessions")} | |
| assert {"document_path", "document_name", "document_mime_type", "document_blocks_json"}.issubset(columns) | |