"""Integration tests for PATCH /ai-sessions/{sid}/suggested-fixes/{fid}/outcome. Fixture style follows test_session_suggested_fixes_api.py: client, test_user, auth_headers, test_db """ from __future__ import annotations import pytest from httpx import AsyncClient from app.models.ai_session import AISession from app.models.session_suggested_fix import SessionSuggestedFix # ── shared helper ──────────────────────────────────────────────────────────── async def _make_session_with_fix(test_db, user) -> tuple[str, str]: """Create an AISession + active proposed SessionSuggestedFix. Returns (session_id_str, fix_id_str). """ session = AISession( user_id=user["user_data"]["id"], account_id=user["user_data"]["account_id"], session_type="chat", intake_type="free_text", intake_content={"text": "outcome test"}, status="active", confidence_tier="discovery", conversation_messages=[], ) test_db.add(session) await test_db.flush() fix = SessionSuggestedFix( session_id=session.id, account_id=session.account_id, title="Reset credential cache", description="Clear stale credentials from the domain cache.", confidence_pct=82, ) test_db.add(fix) await test_db.commit() await test_db.refresh(fix) return str(session.id), str(fix.id) # ── tests ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_patch_outcome_marks_success( client: AsyncClient, test_user, auth_headers, test_db ): session_id, fix_id = await _make_session_with_fix(test_db, test_user) r = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", headers=auth_headers, json={"outcome": "applied_success"}, ) assert r.status_code == 200, r.text body = r.json() assert body["status"] == "applied_success" assert body["verified_at"] is not None @pytest.mark.asyncio async def test_patch_outcome_partial_requires_notes( client: AsyncClient, test_user, auth_headers, test_db ): session_id, fix_id = await _make_session_with_fix(test_db, test_user) r = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", headers=auth_headers, json={"outcome": "applied_partial"}, ) assert r.status_code == 400 assert "notes" in r.text.lower() @pytest.mark.asyncio async def test_partial_to_success_allowed( client: AsyncClient, test_user, auth_headers, test_db ): session_id, fix_id = await _make_session_with_fix(test_db, test_user) r1 = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", headers=auth_headers, json={"outcome": "applied_partial", "notes": "ran cred clear only"}, ) assert r1.status_code == 200, r1.text r2 = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", headers=auth_headers, json={"outcome": "applied_success"}, ) assert r2.status_code == 200 assert r2.json()["status"] == "applied_success" @pytest.mark.asyncio async def test_terminal_outcome_is_locked( client: AsyncClient, test_user, auth_headers, test_db ): session_id, fix_id = await _make_session_with_fix(test_db, test_user) r1 = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", headers=auth_headers, json={"outcome": "applied_failed", "notes": "no change"}, ) assert r1.status_code == 200 r2 = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", headers=auth_headers, json={"outcome": "applied_success"}, ) assert r2.status_code == 409 @pytest.mark.asyncio async def test_partial_notes_can_be_updated( client: AsyncClient, test_user, auth_headers, test_db ): """partial→partial with new notes updates the stored notes.""" session_id, fix_id = await _make_session_with_fix(test_db, test_user) r1 = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", json={"outcome": "applied_partial", "notes": "ran cred clear only"}, headers=auth_headers, ) assert r1.status_code == 200 assert r1.json()["partial_notes"] == "ran cred clear only" r2 = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", json={"outcome": "applied_partial", "notes": "also finished the rebuild; not verified yet"}, headers=auth_headers, ) assert r2.status_code == 200 assert r2.json()["partial_notes"] == "also finished the rebuild; not verified yet" @pytest.mark.asyncio async def test_dismissed_sets_no_timestamps( client: AsyncClient, test_user, auth_headers, test_db ): """dismissed outcome does not stamp applied_at or verified_at.""" session_id, fix_id = await _make_session_with_fix(test_db, test_user) r = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", json={"outcome": "dismissed"}, headers=auth_headers, ) assert r.status_code == 200 body = r.json() assert body["status"] == "dismissed" assert body["applied_at"] is None assert body["verified_at"] is None @pytest.mark.asyncio async def test_applied_at_auto_stamped_on_first_outcome( client: AsyncClient, test_user, auth_headers, test_db ): """If applied_at is null when the engineer sets outcome, server stamps it.""" session_id, fix_id = await _make_session_with_fix(test_db, test_user) r = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", json={"outcome": "applied_success"}, headers=auth_headers, ) assert r.status_code == 200 body = r.json() assert body["applied_at"] is not None assert body["verified_at"] is not None @pytest.mark.asyncio async def test_failed_outcome_stores_notes_as_failure_reason( client: AsyncClient, test_user, auth_headers, test_db ): """applied_failed stores notes under failure_reason (not partial_notes).""" session_id, fix_id = await _make_session_with_fix(test_db, test_user) r = await client.patch( f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome", json={"outcome": "applied_failed", "notes": "user reports no change"}, headers=auth_headers, ) assert r.status_code == 200 body = r.json() assert body["failure_reason"] == "user reports no change" assert body["partial_notes"] is None