test: add Script Builder API tests
Also fix bug in save_to_library: remove invalid 'language' kwarg passed to ScriptTemplate constructor (column doesn't exist on model). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -351,7 +351,6 @@ async def save_to_library(
|
||||
slug=slug,
|
||||
description=description,
|
||||
script_body=session.latest_script,
|
||||
language=session.language,
|
||||
parameters_schema={"parameters": []},
|
||||
default_values={},
|
||||
validation_rules={},
|
||||
|
||||
310
backend/tests/test_script_builder.py
Normal file
310
backend/tests/test_script_builder.py
Normal file
@@ -0,0 +1,310 @@
|
||||
# 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"]
|
||||
Reference in New Issue
Block a user