# backend/tests/test_script_builder.py """Tests for the Script Builder API endpoints.""" import pytest import uuid from unittest.mock import AsyncMock, patch, PropertyMock import sqlalchemy as sa class TestScriptBuilderSessions: """Test Script Builder session CRUD.""" @pytest.mark.asyncio async def test_create_session(self, client, auth_headers): """Creating a builder session returns a valid session.""" resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert data["language"] == "powershell" assert data["messages"] == [] assert data["message_count"] == 0 assert data["title"] is None @pytest.mark.asyncio async def test_create_session_invalid_language(self, client, auth_headers): """Invalid language is rejected.""" resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "cobol"}, headers=auth_headers, ) assert resp.status_code == 422 @pytest.mark.asyncio async def test_create_session_bash(self, client, auth_headers): """Bash language is accepted.""" resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "bash"}, headers=auth_headers, ) assert resp.status_code == 201 assert resp.json()["language"] == "bash" @pytest.mark.asyncio async def test_list_sessions_empty(self, client, auth_headers): """Listing sessions when none exist returns empty list.""" resp = await client.get( "/api/v1/scripts/builder/sessions", headers=auth_headers, ) assert resp.status_code == 200 assert resp.json() == [] @pytest.mark.asyncio async def test_list_sessions_returns_summaries(self, client, auth_headers): """Listed sessions are lightweight (no messages field).""" # Create a session first await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) resp = await client.get( "/api/v1/scripts/builder/sessions", headers=auth_headers, ) assert resp.status_code == 200 data = resp.json() assert len(data) == 1 assert "messages" not in data[0] assert "language" in data[0] assert "title" in data[0] @pytest.mark.asyncio async def test_get_session_detail(self, client, auth_headers): """Getting a session by ID returns full detail with messages.""" create_resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "python"}, headers=auth_headers, ) session_id = create_resp.json()["id"] resp = await client.get( f"/api/v1/scripts/builder/sessions/{session_id}", headers=auth_headers, ) assert resp.status_code == 200 data = resp.json() assert data["id"] == session_id assert "messages" in data assert data["language"] == "python" @pytest.mark.asyncio async def test_get_session_not_found(self, client, auth_headers): """Getting a nonexistent session returns 404.""" fake_id = str(uuid.uuid4()) resp = await client.get( f"/api/v1/scripts/builder/sessions/{fake_id}", headers=auth_headers, ) assert resp.status_code == 404 @pytest.mark.asyncio async def test_delete_session(self, client, auth_headers): """Deleting a session removes it.""" create_resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) session_id = create_resp.json()["id"] resp = await client.delete( f"/api/v1/scripts/builder/sessions/{session_id}", headers=auth_headers, ) assert resp.status_code == 204 # Verify it's gone resp = await client.get( f"/api/v1/scripts/builder/sessions/{session_id}", headers=auth_headers, ) assert resp.status_code == 404 @pytest.mark.asyncio async def test_delete_session_not_found(self, client, auth_headers): """Deleting a nonexistent session returns 404.""" fake_id = str(uuid.uuid4()) resp = await client.delete( f"/api/v1/scripts/builder/sessions/{fake_id}", headers=auth_headers, ) assert resp.status_code == 404 class TestScriptBuilderMessages: """Test sending messages (requires AI mock).""" @pytest.fixture def _enable_ai(self): """Mock AI as enabled for tests without API key.""" with patch.object( type(__import__("app.core.config", fromlist=["settings"]).settings), "ai_enabled", new_callable=PropertyMock, return_value=True, ): yield @pytest.fixture def _mock_ai_response(self): """Mock the AI provider to return a script response.""" mock_response = ( 'Here is your script:\n\n```powershell\nGet-Process | Format-Table\n```\n\nSaved as `Get-Processes.ps1`.', 100, # input_tokens 200, # output_tokens ) with patch("app.services.script_builder_service.get_ai_provider") as mock: provider = AsyncMock() provider.generate_text = AsyncMock(return_value=mock_response) mock.return_value = provider yield @pytest.mark.asyncio async def test_send_message(self, client, auth_headers, _enable_ai, _mock_ai_response): """Sending a message returns AI response with script.""" # Create session create_resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) session_id = create_resp.json()["id"] # Send message resp = await client.post( f"/api/v1/scripts/builder/sessions/{session_id}/messages", json={"content": "List all running processes"}, headers=auth_headers, ) assert resp.status_code == 200 data = resp.json() assert data["role"] == "assistant" assert data["script"] is not None assert "Get-Process" in data["script"] assert data["script_filename"] == "Get-Processes.ps1" assert data["line_count"] == 1 @pytest.mark.asyncio async def test_send_message_updates_session(self, client, auth_headers, _enable_ai, _mock_ai_response): """Sending a message updates session state.""" create_resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) session_id = create_resp.json()["id"] await client.post( f"/api/v1/scripts/builder/sessions/{session_id}/messages", json={"content": "List processes"}, headers=auth_headers, ) # Check session was updated resp = await client.get( f"/api/v1/scripts/builder/sessions/{session_id}", headers=auth_headers, ) data = resp.json() assert data["message_count"] == 1 assert data["latest_script"] is not None assert len(data["messages"]) == 2 # user + assistant assert data["title"] is not None class TestScriptBuilderSaveToLibrary: """Test saving generated scripts to the library.""" @pytest.fixture def _enable_ai(self): with patch.object( type(__import__("app.core.config", fromlist=["settings"]).settings), "ai_enabled", new_callable=PropertyMock, return_value=True, ): yield @pytest.fixture def _mock_ai_response(self): mock_response = ( 'Here is your script:\n\n```powershell\nGet-Process | Format-Table\n```\n\nSaved as `Get-Processes.ps1`.', 100, 200, ) with patch("app.services.script_builder_service.get_ai_provider") as mock: provider = AsyncMock() provider.generate_text = AsyncMock(return_value=mock_response) mock.return_value = provider yield @pytest.fixture async def _seed_ai_category(self, test_db): """Seed the AI Generated category for save tests.""" await test_db.execute( sa.text(""" INSERT INTO script_categories (id, name, slug, description, icon, sort_order, is_active, created_at, updated_at) VALUES ( 'a0000000-0000-0000-0000-000000000001'::uuid, 'AI Generated', 'ai-generated', 'Scripts from AI', 'sparkles', 100, true, NOW(), NOW() ) ON CONFLICT (slug) DO NOTHING """) ) await test_db.commit() @pytest.mark.asyncio async def test_save_to_library_no_script(self, client, auth_headers): """Cannot save if no script has been generated.""" create_resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) session_id = create_resp.json()["id"] resp = await client.post( f"/api/v1/scripts/builder/sessions/{session_id}/save", json={"name": "Test Script"}, headers=auth_headers, ) assert resp.status_code == 400 assert "No script" in resp.json()["detail"] @pytest.mark.asyncio async def test_save_to_library_success( self, client, auth_headers, _enable_ai, _mock_ai_response, _seed_ai_category ): """Saving a generated script creates a ScriptTemplate.""" # Create session and generate a script create_resp = await client.post( "/api/v1/scripts/builder/sessions", json={"language": "powershell"}, headers=auth_headers, ) session_id = create_resp.json()["id"] await client.post( f"/api/v1/scripts/builder/sessions/{session_id}/messages", json={"content": "List processes"}, headers=auth_headers, ) # Save to library resp = await client.post( f"/api/v1/scripts/builder/sessions/{session_id}/save", json={"name": "My Process Script", "share_with_team": False}, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert data["name"] == "My Process Script" assert "ai-generated" in data["tags"]