"""Integration tests for inline pilot_inline script_builder_session behavior. Covers: - Idempotent get-or-create for (user, ai_session_id) on origin='pilot_inline' - Authorization: ai_session_id must belong to current user - list_sessions + count_user_sessions default-scope to 'standalone' """ from __future__ import annotations import pytest from httpx import AsyncClient from sqlalchemy import select, func from uuid import uuid4 from app.models.ai_session import AISession from app.models.script_builder_session import ScriptBuilderSession async def _make_pilot_session(test_db, user) -> str: """Helper: create a minimal pilot session owned by `user`. Matches the existing pattern used by test_fix_outcome_endpoint.py. `user` is the dict returned by the test_user fixture: {"email": ..., "password": ..., "user_data": {"id": ..., "account_id": ..., ...}} """ user_id = user["user_data"]["id"] account_id = user["user_data"]["account_id"] session = AISession( id=uuid4(), user_id=user_id, account_id=account_id, session_type="tshoot", intake_type="psa_ticket", intake_content={}, title="QA", status="active", confidence_tier="exploring", confidence_score=0.0, ) test_db.add(session) await test_db.commit() return str(session.id) @pytest.mark.asyncio async def test_inline_create_is_idempotent( client: AsyncClient, test_user, auth_headers, test_db ): """Second create with same (user, ai_session_id) returns the existing row.""" ai_session_id = await _make_pilot_session(test_db, test_user) r1 = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell", "origin": "pilot_inline", "ai_session_id": ai_session_id}, headers=auth_headers, ) assert r1.status_code in (200, 201), r1.text first_id = r1.json()["id"] r2 = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell", "origin": "pilot_inline", "ai_session_id": ai_session_id}, headers=auth_headers, ) assert r2.status_code in (200, 201) assert r2.json()["id"] == first_id # DB confirms only one row row_count = await test_db.scalar( select(func.count()).select_from(ScriptBuilderSession).where( ScriptBuilderSession.user_id == test_user["user_data"]["id"], ScriptBuilderSession.origin == "pilot_inline", ) ) assert row_count == 1 @pytest.mark.asyncio async def test_inline_requires_ai_session_id( client: AsyncClient, auth_headers ): """origin='pilot_inline' without ai_session_id is rejected.""" r = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell", "origin": "pilot_inline"}, headers=auth_headers, ) assert r.status_code == 400 assert "ai_session_id" in r.text.lower() @pytest.mark.asyncio async def test_inline_ai_session_must_belong_to_caller( client: AsyncClient, test_user, auth_headers, test_db ): """ai_session_id pointing at another user's session is rejected.""" # Create pilot session owned by a DIFFERENT user from app.models.user import User from app.models.account import Account other_account = Account(id=uuid4(), name="other", display_code="OTH-0001") test_db.add(other_account) await test_db.flush() other_user = User( id=uuid4(), email="other@example.com", password_hash="x", name="Other", role="engineer", is_super_admin=False, is_team_admin=False, is_active=True, is_service_account=False, must_change_password=False, account_id=other_account.id, account_role="engineer", ) test_db.add(other_user) await test_db.flush() # Build user dict in the same shape as the test_user fixture other_user_dict = { "user_data": {"id": str(other_user.id), "account_id": str(other_account.id)} } other_session_id = await _make_pilot_session(test_db, other_user_dict) r = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell", "origin": "pilot_inline", "ai_session_id": other_session_id}, headers=auth_headers, ) assert r.status_code in (403, 404), r.text @pytest.mark.asyncio async def test_list_sessions_excludes_inline( client: AsyncClient, test_user, auth_headers, test_db ): """GET /scripts/builder/sessions returns only standalone rows.""" ai_session_id = await _make_pilot_session(test_db, test_user) # Create one inline session await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell", "origin": "pilot_inline", "ai_session_id": ai_session_id}, headers=auth_headers, ) # Create one standalone session await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) r = await client.get("/api/v1/scripts/builder/sessions", headers=auth_headers) assert r.status_code == 200 body = r.json() # Depending on response shape, this may be a list or {"sessions": [...]}. items = body if isinstance(body, list) else body.get("sessions", body.get("items", [])) # Response schema does not surface `origin`; len==1 is the only meaningful guard: # inline row would push this to 2. assert len(items) == 1 @pytest.mark.asyncio async def test_inline_sessions_do_not_count_against_cap( client: AsyncClient, test_user, auth_headers, test_db ): """Creating 5 pilot_inline sessions does not block a subsequent standalone.""" # Create 5 distinct pilot sessions and attach inline builder sessions to each for _ in range(5): ai_session_id = await _make_pilot_session(test_db, test_user) r = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell", "origin": "pilot_inline", "ai_session_id": ai_session_id}, headers=auth_headers, ) assert r.status_code in (200, 201), r.text # A standalone create should still succeed — inline sessions don't count r = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) assert r.status_code in (200, 201), r.text