feat(api): add content generation endpoints (Sprint 4)

Implement Sprint 4: Content Generation

- Add ArtifactService with generation methods for 9 content types
- Add POST /generate/audio - Generate podcast
- Add POST /generate/video - Generate video
- Add POST /generate/slide-deck - Generate slides
- Add POST /generate/infographic - Generate infographic
- Add POST /generate/quiz - Generate quiz
- Add POST /generate/flashcards - Generate flashcards
- Add POST /generate/report - Generate report
- Add POST /generate/mind-map - Generate mind map (instant)
- Add POST /generate/data-table - Generate data table
- Add GET /artifacts - List artifacts
- Add GET /artifacts/{id}/status - Check artifact status

Models:
- AudioGenerationRequest, VideoGenerationRequest
- QuizGenerationRequest, FlashcardsGenerationRequest
- SlideDeckGenerationRequest, InfographicGenerationRequest
- ReportGenerationRequest, DataTableGenerationRequest
- Artifact, GenerationResponse, ArtifactList

Tests:
- 13 unit tests for ArtifactService
- 6 integration tests for generation API
- 19/19 tests passing

Related: Sprint 4 - Content Generation
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-06 01:58:47 +02:00
parent 081f3f0d89
commit 83fd30a2a2
8 changed files with 2184 additions and 1 deletions

View File

@@ -0,0 +1,292 @@
"""Unit tests for ArtifactService.
TDD Cycle: RED → GREEN → REFACTOR
"""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock
from uuid import UUID, uuid4
import pytest
from notebooklm_agent.core.exceptions import (
NotebookLMError,
NotFoundError,
)
from notebooklm_agent.services.artifact_service import ArtifactService
@pytest.mark.unit
class TestArtifactServiceInit:
"""Test suite for ArtifactService initialization."""
async def test_get_client_returns_existing_client(self):
"""Should return existing client if already initialized."""
# Arrange
mock_client = AsyncMock()
service = ArtifactService(client=mock_client)
# Act
client = await service._get_client()
# Assert
assert client == mock_client
@pytest.mark.unit
class TestArtifactServiceGenerateAudio:
"""Test suite for generate_audio method."""
async def test_generate_audio_returns_generation_response(self):
"""Should start audio generation and return response."""
# Arrange
notebook_id = uuid4()
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_result = MagicMock()
mock_result.id = str(uuid4())
mock_result.status = "processing"
mock_notebook.generate_audio.return_value = mock_result
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act
result = await service.generate_audio(
notebook_id,
instructions="Make it engaging",
format="deep-dive",
length="long",
language="en",
)
# Assert
assert result.status == "processing"
assert str(result.artifact_id) == str(mock_result.id)
assert result.estimated_time_seconds == 600
assert "Audio generation started" in result.message
async def test_generate_audio_notebook_not_found_raises_not_found(self):
"""Should raise NotFoundError if notebook not found."""
# Arrange
notebook_id = uuid4()
mock_client = AsyncMock()
mock_client.notebooks.get.side_effect = Exception("notebook not found")
service = ArtifactService(client=mock_client)
# Act & Assert
with pytest.raises(NotFoundError) as exc_info:
await service.generate_audio(notebook_id)
assert str(notebook_id) in str(exc_info.value)
@pytest.mark.unit
class TestArtifactServiceGenerateVideo:
"""Test suite for generate_video method."""
async def test_generate_video_returns_generation_response(self):
"""Should start video generation and return response."""
# Arrange
notebook_id = uuid4()
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_result = MagicMock()
mock_result.id = str(uuid4())
mock_result.status = "processing"
mock_notebook.generate_video.return_value = mock_result
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act
result = await service.generate_video(
notebook_id,
instructions="Create engaging video",
style="whiteboard",
language="en",
)
# Assert
assert result.status == "processing"
assert result.estimated_time_seconds == 1800
@pytest.mark.unit
class TestArtifactServiceGenerateQuiz:
"""Test suite for generate_quiz method."""
async def test_generate_quiz_returns_generation_response(self):
"""Should start quiz generation and return response."""
# Arrange
notebook_id = uuid4()
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_result = MagicMock()
mock_result.id = str(uuid4())
mock_result.status = "processing"
mock_notebook.generate_quiz.return_value = mock_result
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act
result = await service.generate_quiz(
notebook_id,
difficulty="medium",
quantity="standard",
)
# Assert
assert result.status == "processing"
assert result.estimated_time_seconds == 600
@pytest.mark.unit
class TestArtifactServiceGenerateMindMap:
"""Test suite for generate_mind_map method."""
async def test_generate_mind_map_returns_generation_response(self):
"""Should generate mind map (instant) and return response."""
# Arrange
notebook_id = uuid4()
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_result = MagicMock()
mock_result.id = str(uuid4())
mock_result.status = "completed"
mock_notebook.generate_mind_map.return_value = mock_result
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act
result = await service.generate_mind_map(notebook_id)
# Assert
assert result.status == "completed"
assert result.estimated_time_seconds == 10
@pytest.mark.unit
class TestArtifactServiceListArtifacts:
"""Test suite for list_artifacts method."""
async def test_list_artifacts_returns_list(self):
"""Should return list of artifacts."""
# Arrange
notebook_id = uuid4()
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_artifact = MagicMock()
mock_artifact.id = str(uuid4())
mock_artifact.type = "audio"
mock_artifact.title = "Podcast"
mock_artifact.status = "completed"
mock_artifact.created_at = datetime.utcnow()
mock_artifact.completed_at = datetime.utcnow()
mock_artifact.download_url = "https://example.com/download"
mock_notebook.artifacts.list.return_value = [mock_artifact]
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act
result = await service.list_artifacts(notebook_id)
# Assert
assert len(result) == 1
assert result[0].type == "audio"
assert result[0].status == "completed"
async def test_list_artifacts_empty_returns_empty_list(self):
"""Should return empty list if no artifacts."""
# Arrange
notebook_id = uuid4()
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_notebook.artifacts.list.return_value = []
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act
result = await service.list_artifacts(notebook_id)
# Assert
assert result == []
@pytest.mark.unit
class TestArtifactServiceGetStatus:
"""Test suite for get_status method."""
async def test_get_status_returns_artifact(self):
"""Should return artifact with status."""
# Arrange
notebook_id = uuid4()
artifact_id = str(uuid4())
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_artifact = MagicMock()
mock_artifact.id = artifact_id
mock_artifact.type = "video"
mock_artifact.title = "Video"
mock_artifact.status = "processing"
mock_artifact.created_at = datetime.utcnow()
mock_artifact.completed_at = None
mock_artifact.download_url = None
mock_notebook.artifacts.get.return_value = mock_artifact
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act
result = await service.get_status(notebook_id, artifact_id)
# Assert
assert str(result.id) == artifact_id
assert result.type == "video"
assert result.status == "processing"
async def test_get_status_artifact_not_found_raises_not_found(self):
"""Should raise NotFoundError if artifact not found."""
# Arrange
notebook_id = uuid4()
artifact_id = str(uuid4())
mock_client = AsyncMock()
mock_notebook = AsyncMock()
mock_notebook.artifacts.get.side_effect = Exception("artifact not found")
mock_client.notebooks.get.return_value = mock_notebook
service = ArtifactService(client=mock_client)
# Act & Assert
with pytest.raises(NotFoundError) as exc_info:
await service.get_status(notebook_id, artifact_id)
assert artifact_id in str(exc_info.value)
@pytest.mark.unit
class TestArtifactServiceEstimatedTimes:
"""Test suite for estimated times."""
def test_estimated_times_defined(self):
"""Should have estimated times for all artifact types."""
# Arrange
service = ArtifactService()
# Assert
assert service.ESTIMATED_TIMES["audio"] == 600
assert service.ESTIMATED_TIMES["video"] == 1800
assert service.ESTIMATED_TIMES["mind-map"] == 10