"""Tests for survey endpoints: vision health, analyze, save response, get history.""" import pytest from unittest.mock import patch, MagicMock from fastapi.testclient import TestClient @pytest.fixture def client(): import sys sys.path.insert(0, "/Library/Development/CircuitForge/peregrine/.worktrees/feature-vue-spa") from dev_api import app return TestClient(app) # ── GET /api/vision/health ─────────────────────────────────────────────────── def test_vision_health_available(client): """Returns available=true when vision service responds 200.""" mock_resp = MagicMock() mock_resp.status_code = 200 with patch("dev_api.requests.get", return_value=mock_resp): resp = client.get("/api/vision/health") assert resp.status_code == 200 assert resp.json() == {"available": True} def test_vision_health_unavailable(client): """Returns available=false when vision service times out or errors.""" with patch("dev_api.requests.get", side_effect=Exception("timeout")): resp = client.get("/api/vision/health") assert resp.status_code == 200 assert resp.json() == {"available": False} # ── POST /api/jobs/{id}/survey/analyze ────────────────────────────────────── def test_analyze_text_quick(client): """Text mode quick analysis returns output and source=text_paste.""" mock_router = MagicMock() mock_router.complete.return_value = "1. B — best option" mock_router.config.get.return_value = ["claude_code", "vllm"] with patch("dev_api.LLMRouter", return_value=mock_router): resp = client.post("/api/jobs/1/survey/analyze", json={ "text": "Q1: Do you prefer teamwork?\nA. Solo B. Together", "mode": "quick", }) assert resp.status_code == 200 data = resp.json() assert data["source"] == "text_paste" assert "B" in data["output"] # System prompt must be passed for text path call_kwargs = mock_router.complete.call_args[1] assert "system" in call_kwargs assert "culture-fit survey" in call_kwargs["system"] def test_analyze_text_detailed(client): """Text mode detailed analysis passes correct prompt.""" mock_router = MagicMock() mock_router.complete.return_value = "Option A: good for... Option B: better because..." mock_router.config.get.return_value = [] with patch("dev_api.LLMRouter", return_value=mock_router): resp = client.post("/api/jobs/1/survey/analyze", json={ "text": "Q1: Describe your work style.", "mode": "detailed", }) assert resp.status_code == 200 assert resp.json()["source"] == "text_paste" def test_analyze_image(client): """Image mode routes through vision path with NO system prompt.""" mock_router = MagicMock() mock_router.complete.return_value = "1. C — collaborative choice" mock_router.config.get.return_value = ["vision_service", "claude_code"] with patch("dev_api.LLMRouter", return_value=mock_router): resp = client.post("/api/jobs/1/survey/analyze", json={ "image_b64": "aGVsbG8=", "mode": "quick", }) assert resp.status_code == 200 data = resp.json() assert data["source"] == "screenshot" # No system prompt on vision path call_kwargs = mock_router.complete.call_args[1] assert "system" not in call_kwargs def test_analyze_llm_failure(client): """Returns 500 when LLM raises an exception.""" mock_router = MagicMock() mock_router.complete.side_effect = Exception("LLM unavailable") mock_router.config.get.return_value = [] with patch("dev_api.LLMRouter", return_value=mock_router): resp = client.post("/api/jobs/1/survey/analyze", json={ "text": "Q1: test", "mode": "quick", }) assert resp.status_code == 500 # ── POST /api/jobs/{id}/survey/responses ──────────────────────────────────── def test_save_response_text(client): """Save text response writes to DB and returns id.""" mock_db = MagicMock() with patch("dev_api._get_db", return_value=mock_db): with patch("dev_api.insert_survey_response", return_value=42) as mock_insert: resp = client.post("/api/jobs/1/survey/responses", json={ "mode": "quick", "source": "text_paste", "raw_input": "Q1: test question", "llm_output": "1. B — good reason", }) assert resp.status_code == 200 assert resp.json()["id"] == 42 # received_at generated by backend — not None call_args = mock_insert.call_args assert call_args[1]["received_at"] is not None or call_args[0][3] is not None def test_save_response_with_image(client, tmp_path, monkeypatch): """Save image response writes PNG file and stores path in DB.""" monkeypatch.setenv("STAGING_DB", str(tmp_path / "test.db")) with patch("dev_api.insert_survey_response", return_value=7) as mock_insert: with patch("dev_api.Path") as mock_path_cls: mock_path_cls.return_value.__truediv__ = lambda s, o: tmp_path / o resp = client.post("/api/jobs/1/survey/responses", json={ "mode": "quick", "source": "screenshot", "image_b64": "aGVsbG8=", # valid base64 "llm_output": "1. B — reason", }) assert resp.status_code == 200 assert resp.json()["id"] == 7 # ── GET /api/jobs/{id}/survey/responses ───────────────────────────────────── def test_get_history_empty(client): """Returns empty list when no history exists.""" with patch("dev_api.get_survey_responses", return_value=[]): resp = client.get("/api/jobs/1/survey/responses") assert resp.status_code == 200 assert resp.json() == [] def test_get_history_populated(client): """Returns history rows newest first.""" rows = [ {"id": 2, "survey_name": "Round 2", "mode": "detailed", "source": "text_paste", "raw_input": None, "image_path": None, "llm_output": "Option A is best", "reported_score": "90%", "received_at": "2026-03-21T14:00:00", "created_at": "2026-03-21T14:00:01"}, {"id": 1, "survey_name": "Round 1", "mode": "quick", "source": "text_paste", "raw_input": "Q1: test", "image_path": None, "llm_output": "1. B", "reported_score": None, "received_at": "2026-03-21T12:00:00", "created_at": "2026-03-21T12:00:01"}, ] with patch("dev_api.get_survey_responses", return_value=rows): resp = client.get("/api/jobs/1/survey/responses") assert resp.status_code == 200 data = resp.json() assert len(data) == 2 assert data[0]["id"] == 2 assert data[0]["survey_name"] == "Round 2"