09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
261 lines
10 KiB
Python
261 lines
10 KiB
Python
"""Artifact CRUD and retrieval tests.
|
|
|
|
Generation tests are in test_generation.py. This file contains:
|
|
- Artifact listing and retrieval
|
|
- Artifact mutations (delete, rename)
|
|
- Report suggestions
|
|
- Status polling
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from notebooklm import Artifact, ArtifactType, ReportSuggestion
|
|
from notebooklm.exceptions import ArtifactNotFoundError, RPCTimeoutError
|
|
|
|
from .conftest import assert_generation_started, requires_auth
|
|
|
|
|
|
@requires_auth
|
|
class TestArtifactRetrieval:
|
|
"""Tests for artifact retrieval and listing operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_artifacts(self, client, read_only_notebook_id):
|
|
"""Read-only test - lists existing artifacts."""
|
|
artifacts = await client.artifacts.list(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
assert all(isinstance(art, Artifact) for art in artifacts)
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_get_artifact(self, client, read_only_notebook_id):
|
|
"""Test getting a specific artifact by ID."""
|
|
artifacts = await client.artifacts.list(read_only_notebook_id)
|
|
if not artifacts:
|
|
pytest.skip("No artifacts available to get")
|
|
|
|
artifact = await client.artifacts.get(read_only_notebook_id, artifacts[0].id)
|
|
assert artifact is not None
|
|
assert isinstance(artifact, Artifact)
|
|
assert artifact.id == artifacts[0].id
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_get_artifact_not_found(self, client, read_only_notebook_id):
|
|
"""Test getting a non-existent artifact raises ArtifactNotFoundError."""
|
|
# v0.8.0: a miss now raises ArtifactNotFoundError (issue #1247).
|
|
with pytest.raises(ArtifactNotFoundError):
|
|
await client.artifacts.get(read_only_notebook_id, "nonexistent_artifact_id")
|
|
|
|
|
|
@requires_auth
|
|
class TestArtifactTypeSpecificLists:
|
|
"""Tests for type-specific artifact list methods."""
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_audio(self, client, read_only_notebook_id):
|
|
"""Test listing audio artifacts."""
|
|
artifacts = await client.artifacts.list_audio(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be audio type
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.AUDIO
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_video(self, client, read_only_notebook_id):
|
|
"""Test listing video artifacts."""
|
|
artifacts = await client.artifacts.list_video(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be video type
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.VIDEO
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_reports(self, client, read_only_notebook_id):
|
|
"""Test listing report artifacts."""
|
|
artifacts = await client.artifacts.list_reports(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be report type
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.REPORT
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_quizzes(self, client, read_only_notebook_id):
|
|
"""Test listing quiz artifacts."""
|
|
artifacts = await client.artifacts.list_quizzes(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be quizzes
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.QUIZ
|
|
assert art.is_quiz is True
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_flashcards(self, client, read_only_notebook_id):
|
|
"""Test listing flashcard artifacts."""
|
|
artifacts = await client.artifacts.list_flashcards(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be flashcards
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.FLASHCARDS
|
|
assert art.is_flashcards is True
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_infographics(self, client, read_only_notebook_id):
|
|
"""Test listing infographic artifacts."""
|
|
artifacts = await client.artifacts.list_infographics(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be infographic type
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.INFOGRAPHIC
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_slide_decks(self, client, read_only_notebook_id):
|
|
"""Test listing slide deck artifacts."""
|
|
artifacts = await client.artifacts.list_slide_decks(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be slide deck type
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.SLIDE_DECK
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_list_data_tables(self, client, read_only_notebook_id):
|
|
"""Test listing data table artifacts."""
|
|
artifacts = await client.artifacts.list_data_tables(read_only_notebook_id)
|
|
assert isinstance(artifacts, list)
|
|
# All returned should be data table type
|
|
for art in artifacts:
|
|
assert art.kind == ArtifactType.DATA_TABLE
|
|
|
|
|
|
@requires_auth
|
|
class TestReportSuggestions:
|
|
"""Report suggestion tests."""
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.readonly
|
|
async def test_suggest_reports(self, client, read_only_notebook_id):
|
|
"""Read-only test - gets suggestions without generating."""
|
|
try:
|
|
suggestions = await client.artifacts.suggest_reports(read_only_notebook_id)
|
|
except RPCTimeoutError:
|
|
pytest.skip("GET_SUGGESTED_REPORTS timed out - API may be rate limited")
|
|
|
|
assert isinstance(suggestions, list)
|
|
if suggestions:
|
|
assert all(isinstance(s, ReportSuggestion) for s in suggestions)
|
|
for s in suggestions:
|
|
assert s.title
|
|
assert s.description
|
|
assert s.prompt
|
|
|
|
|
|
@requires_auth
|
|
class TestArtifactMutations:
|
|
"""Tests that modify/delete artifacts.
|
|
|
|
Combines poll/rename/wait into one test to reuse a single flashcard artifact.
|
|
Delete test uses a separate quiz artifact to spread rate limits.
|
|
"""
|
|
|
|
@pytest.mark.skip(
|
|
reason="Replaced by VCR-based replay in tests/integration/test_polling_vcr.py: "
|
|
"the live generate + wait_for_completion flow regularly exceeded "
|
|
"the 60s pytest timeout. The new test records the polling sequence once "
|
|
"(real wall-clock) and replays it offline with asyncio.sleep monkey-patched "
|
|
"to a no-op, so the same coverage runs in <2s on every CI build."
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_poll_rename_wait(self, client, temp_notebook):
|
|
"""Test poll_status, rename, and wait_for_completion on ONE artifact.
|
|
|
|
Combines three operations into one test to minimize API calls:
|
|
1. Generate one flashcard artifact
|
|
2. Poll its status
|
|
3. Wait for completion
|
|
4. Rename it and rename back
|
|
|
|
Uses flashcards (more reliable than quiz for generation).
|
|
|
|
.. note::
|
|
See ``tests/integration/test_polling_vcr.py`` for the cassette-backed
|
|
replacement that exercises the same flow without the 60s timeout
|
|
risk. This e2e remains in the file as living documentation of the
|
|
real-API sequence; rerun it manually if you need to re-record the
|
|
cassette and want to validate the live flow first.
|
|
"""
|
|
# Generate ONE artifact for all operations
|
|
result = await client.artifacts.generate_flashcards(temp_notebook.id)
|
|
assert_generation_started(result, "Flashcard")
|
|
notebook_id = temp_notebook.id
|
|
artifact_id = result.task_id
|
|
|
|
# 1. Test poll_status
|
|
await asyncio.sleep(2)
|
|
status = await client.artifacts.poll_status(notebook_id, artifact_id)
|
|
assert status is not None
|
|
assert hasattr(status, "status")
|
|
|
|
# 2. Test wait_for_completion
|
|
final_status = await client.artifacts.wait_for_completion(
|
|
notebook_id,
|
|
artifact_id,
|
|
initial_interval=1.0,
|
|
max_interval=5.0,
|
|
timeout=60.0,
|
|
)
|
|
assert final_status is not None
|
|
assert final_status.is_complete or final_status.is_failed
|
|
|
|
# 3. Test rename (only if artifact completed successfully)
|
|
if final_status.is_complete:
|
|
artifact = await client.artifacts.get(notebook_id, artifact_id)
|
|
if artifact:
|
|
original_title = artifact.title
|
|
|
|
# Rename to new title
|
|
new_title = "Renamed E2E Test"
|
|
await client.artifacts.rename(notebook_id, artifact_id, new_title)
|
|
|
|
# Verify rename
|
|
await asyncio.sleep(1)
|
|
renamed = await client.artifacts.get(notebook_id, artifact_id)
|
|
assert renamed is not None
|
|
assert renamed.title == new_title
|
|
|
|
# Restore original title
|
|
await client.artifacts.rename(notebook_id, artifact_id, original_title)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_artifact(self, client, temp_notebook):
|
|
"""Test deleting an artifact.
|
|
|
|
Uses quiz instead of flashcards to spread rate limits across different
|
|
artifact type quotas.
|
|
"""
|
|
# Create a quiz artifact for deletion (different type than flashcards)
|
|
result = await client.artifacts.generate_quiz(temp_notebook.id)
|
|
assert_generation_started(result, "Quiz")
|
|
artifact_id = result.task_id
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
# Delete it (v0.7.0: returns None, idempotent — issue #1211)
|
|
deleted = await client.artifacts.delete(temp_notebook.id, artifact_id)
|
|
assert deleted is None
|
|
|
|
# Verify it's gone
|
|
artifacts = await client.artifacts.list(temp_notebook.id)
|
|
artifact_ids = [a.id for a in artifacts]
|
|
assert artifact_id not in artifact_ids
|