Files
wehub-resource-sync 09e9f3545f
CodeQL / Analyze (push) Waiting to run
dependency-audit / pip-audit (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

321 lines
11 KiB
Python

"""Unit tests for new API coverage features."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from notebooklm._artifacts import ArtifactsAPI
from notebooklm._chat import ChatAPI
from notebooklm._mind_map import NoteBackedMindMapService
from notebooklm._note_service import NoteService
from notebooklm._notebooks import NotebooksAPI
from notebooklm._runtime.contracts import LoopGuard
from notebooklm._sources import SourcesAPI
from notebooklm.rpc.types import (
ChatGoal,
ChatResponseLength,
DriveMimeType,
RPCMethod,
)
from tests._fixtures.fake_core import make_fake_core
class TestNewEnums:
"""Tests for newly added enums."""
def test_chat_goal_values(self):
"""Test ChatGoal enum values match API spec."""
assert ChatGoal.DEFAULT == 1
assert ChatGoal.CUSTOM == 2
assert ChatGoal.LEARNING_GUIDE == 3
def test_chat_response_length_values(self):
"""Test ChatResponseLength enum values match API spec."""
assert ChatResponseLength.DEFAULT == 1
assert ChatResponseLength.LONGER == 4
assert ChatResponseLength.SHORTER == 5
def test_drive_mime_type_values(self):
"""Test DriveMimeType enum values."""
assert DriveMimeType.GOOGLE_DOC == "application/vnd.google-apps.document"
assert DriveMimeType.GOOGLE_SLIDES == "application/vnd.google-apps.presentation"
assert DriveMimeType.GOOGLE_SHEETS == "application/vnd.google-apps.spreadsheet"
assert DriveMimeType.PDF == "application/pdf"
def test_get_suggested_reports_rpc_id(self):
"""Test GET_SUGGESTED_REPORTS RPC ID exists."""
assert RPCMethod.GET_SUGGESTED_REPORTS == "ciyUvf"
class TestConfigureChat:
"""Tests for ChatAPI.configure."""
@pytest.fixture
def rpc_call(self):
"""RPC seam injected at construction (ADR-0007)."""
return AsyncMock(return_value=None)
@pytest.fixture
def chat(self, rpc_call):
"""Create a ``ChatAPI`` wired to the injected RPC seam.
The ``rpc_call`` mock is injected via ``make_fake_core(rpc_call=...)``
/ the ``ChatAPI`` constructor rather than assigned onto an instance
attribute after the fact, satisfying the ADR-0007 monkeypatch policy.
``configure`` only exercises the ``rpc`` collaborator; the remaining
constructor collaborators are inert ``MagicMock`` stand-ins.
"""
core = make_fake_core(rpc_call=rpc_call)
return ChatAPI(
rpc=core.rpc_executor,
transport=MagicMock(),
reqid=MagicMock(),
loop_guard=MagicMock(spec=LoopGuard),
)
@pytest.mark.asyncio
async def test_configure_chat_default(self, chat, rpc_call):
"""Test configure with default settings."""
await chat.configure("notebook_123")
rpc_call.assert_called_once()
call_args = rpc_call.call_args
params = call_args[0][1]
# Verify payload structure
assert params[0] == "notebook_123"
assert params[1][0][7] == [[1], [1]] # Default goal, default length
@pytest.mark.asyncio
async def test_configure_chat_custom_prompt(self, chat, rpc_call):
"""Test configure with custom prompt."""
await chat.configure(
"notebook_123",
goal=ChatGoal.CUSTOM,
custom_prompt="Be an expert analyst",
)
call_args = rpc_call.call_args
params = call_args[0][1]
# Verify custom prompt is included
assert params[1][0][7][0] == [2, "Be an expert analyst"]
@pytest.mark.asyncio
async def test_configure_chat_custom_requires_prompt(self, chat):
"""Test configure raises when CUSTOM goal has no prompt."""
from notebooklm.exceptions import ValidationError
with pytest.raises(ValidationError, match="custom_prompt is required"):
await chat.configure(
"notebook_123",
goal=ChatGoal.CUSTOM,
)
@pytest.mark.asyncio
async def test_configure_chat_learning_guide(self, chat, rpc_call):
"""Test configure with learning-guide mode."""
await chat.configure(
"notebook_123",
goal=ChatGoal.LEARNING_GUIDE,
response_length=ChatResponseLength.LONGER,
)
call_args = rpc_call.call_args
params = call_args[0][1]
assert params[1][0][7] == [[3], [4]] # Learning guide, longer
class TestGetSourceGuide:
"""Tests for SourcesAPI.get_guide."""
def _make_sources(self, return_value):
"""Build a ``SourcesAPI`` with the RPC seam injected at construction.
Returns ``(sources, rpc_call)`` so tests can assert the seam was
exercised. ``make_fake_core(rpc_call=...)`` is the sanctioned
constructor-injection substrate (ADR-0007); ``get_guide`` routes
through the injected ``rpc`` collaborator only.
"""
rpc_call = AsyncMock(return_value=return_value)
core = make_fake_core(rpc_call=rpc_call)
sources = SourcesAPI(core.rpc_executor, uploader=MagicMock())
return sources, rpc_call
@pytest.mark.asyncio
async def test_get_source_guide_parses_response(self):
"""Test get_guide correctly parses API response."""
# Real API returns 3 levels of nesting: [[[null, [summary], [[keywords]], []]]]
mock_response = [
[
[
None,
["This is a **summary** of the document."],
[["Topic 1", "Topic 2", "Topic 3"]],
[],
]
]
]
sources, rpc_call = self._make_sources(mock_response)
result = await sources.get_guide("notebook_123", "source_456")
rpc_call.assert_called_once()
assert result.summary == "This is a **summary** of the document."
assert result.keywords == ("Topic 1", "Topic 2", "Topic 3")
@pytest.mark.asyncio
async def test_get_source_guide_handles_empty(self):
"""Test get_guide handles an empty response."""
sources, rpc_call = self._make_sources(None)
result = await sources.get_guide("notebook_123", "source_456")
rpc_call.assert_called_once()
assert result.summary == ""
assert result.keywords == ()
class TestGetSuggestedReportFormats:
"""Tests for ArtifactsAPI.suggest_reports."""
@pytest.mark.asyncio
async def test_get_suggested_report_formats_parses_response(self):
"""Test suggest_reports correctly parses API response."""
# Response format: [[[title, description, null, null, prompt, audience_level], ...]]
mock_response = [
[
["Strategy Report", "Analysis of...", None, None, "Create a detailed...", 2],
["Summary Brief", "Quick overview...", None, None, "Summarize the...", 1],
]
]
rpc_call = AsyncMock(return_value=mock_response)
core = make_fake_core(rpc_call=rpc_call)
artifacts = ArtifactsAPI(
rpc=core.rpc_executor,
drain=core,
lifecycle=core,
notebooks=MagicMock(),
mind_maps=MagicMock(spec=NoteBackedMindMapService),
note_service=MagicMock(spec=NoteService),
)
result = await artifacts.suggest_reports("notebook_123")
rpc_call.assert_called_once()
assert len(result) == 2
assert result[0].title == "Strategy Report"
assert result[0].description == "Analysis of..."
assert result[0].prompt == "Create a detailed..."
class TestAddSourceDrive:
"""Tests for SourcesAPI.add_drive."""
@pytest.mark.asyncio
async def test_add_source_drive_payload_structure(self):
"""Test add_drive creates the expected payload."""
rpc_call = AsyncMock(return_value=[["source_id_123"]])
core = make_fake_core(rpc_call=rpc_call)
sources = SourcesAPI(core.rpc_executor, uploader=MagicMock())
await sources.add_drive(
"notebook_123",
file_id="drive_file_abc",
title="My Document",
mime_type=DriveMimeType.GOOGLE_DOC.value,
)
rpc_call.assert_called_once()
call_args = rpc_call.call_args
params = call_args[0][1]
# Verify source data structure - params[0] is [source_data] (single wrap)
source_data = params[0][0]
assert source_data[0] == [
"drive_file_abc",
"application/vnd.google-apps.document",
1,
"My Document",
]
assert source_data[10] == 1 # Trailing 1
class TestGetNotebookDescription:
"""Tests for NotebooksAPI.get_description."""
@pytest.mark.asyncio
async def test_get_notebook_description_parses_response(self):
"""Test get_description parses the full response."""
mock_response = [
[
["This notebook explores **AI** and **machine learning**."],
[
[
["What is the future of AI?", "Create a detailed briefing..."],
["How does ML work?", "Explain the fundamentals..."],
]
],
]
]
rpc_call = AsyncMock(return_value=mock_response)
core = make_fake_core(rpc_call=rpc_call)
notebooks = NotebooksAPI(core.rpc_executor, sources_api=MagicMock())
result = await notebooks.get_description("notebook_123")
rpc_call.assert_called_once()
assert "AI" in result.summary
assert len(result.suggested_topics) == 2
assert result.suggested_topics[0].question == "What is the future of AI?"
assert "briefing" in result.suggested_topics[0].prompt
class TestPayloadFixes:
"""Tests for fixed payload structures."""
def _make_sources(self):
"""Build a ``SourcesAPI`` with the RPC seam injected at construction.
Returns ``(sources, rpc_call)``; the ``True`` return value mimics
the freshness/refresh RPC acknowledgements. Constructor injection
via ``make_fake_core(rpc_call=...)`` keeps the test ADR-0007-clean.
"""
rpc_call = AsyncMock(return_value=True)
core = make_fake_core(rpc_call=rpc_call)
sources = SourcesAPI(core.rpc_executor, uploader=MagicMock())
return sources, rpc_call
@pytest.mark.asyncio
async def test_check_source_freshness_payload(self):
"""Test check_source_freshness uses correct payload structure."""
sources, rpc_call = self._make_sources()
await sources.check_freshness("notebook_123", "source_456")
rpc_call.assert_called_once()
call_args = rpc_call.call_args
params = call_args[0][1]
# Verify reference payload: [null, ["source_id"], [2]]
assert params[0] is None
assert params[1] == ["source_456"]
assert params[2] == [2]
@pytest.mark.asyncio
async def test_refresh_source_payload(self):
"""Test refresh_source uses correct payload structure."""
sources, rpc_call = self._make_sources()
await sources.refresh("notebook_123", "source_456")
rpc_call.assert_called_once()
call_args = rpc_call.call_args
params = call_args[0][1]
# Verify reference payload: [null, ["source_id"], [2]]
assert params[0] is None
assert params[1] == ["source_456"]
assert params[2] == [2]