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
188 lines
6.6 KiB
Python
188 lines
6.6 KiB
Python
"""Unit tests for the transport-neutral ``notebooklm._app.source_content`` core.
|
|
|
|
These pin the read-only source-content fetchers at the ``_app`` boundary
|
|
(independent of the Click adapter): each executor returns a typed result the
|
|
transport adapter renders into its own envelope vocabulary.
|
|
|
|
* :func:`execute_source_get` — single-source fetch via ``get_or_none`` (``None``
|
|
when the backend no longer has it).
|
|
* :func:`execute_source_fulltext` — fulltext fetch, ``output_format`` threaded
|
|
through.
|
|
* :func:`execute_source_guide` — guide summary + keyword normalisation (the
|
|
keyword strip/filter + the :attr:`SourceGuideResult.is_empty` projection).
|
|
* :func:`execute_source_stale` — freshness predicate + the
|
|
:attr:`SourceStaleResult.stale` inversion.
|
|
|
|
Pure-service tests (no Click / CliRunner): the command-layer rendering /
|
|
exit-code policy is exercised in
|
|
``tests/unit/cli/test_source_content_rendering.py`` and
|
|
``tests/unit/cli/test_source_refresh.py``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from notebooklm._app.source_content import (
|
|
SourceFulltextPlan,
|
|
SourceGetPlan,
|
|
SourceGuidePlan,
|
|
SourceGuideResult,
|
|
SourceStalePlan,
|
|
SourceStaleResult,
|
|
execute_source_fulltext,
|
|
execute_source_get,
|
|
execute_source_guide,
|
|
execute_source_stale,
|
|
)
|
|
from notebooklm.types import Source, SourceFulltext, SourceGuide
|
|
|
|
|
|
def _client() -> MagicMock:
|
|
client = MagicMock()
|
|
client.sources = MagicMock()
|
|
return client
|
|
|
|
|
|
# ===========================================================================
|
|
# execute_source_get
|
|
# ===========================================================================
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_returns_source() -> None:
|
|
client = _client()
|
|
src = Source(id="src_1", title="One")
|
|
client.sources.get_or_none = AsyncMock(return_value=src)
|
|
result = await execute_source_get(client, SourceGetPlan(notebook_id="nb_1", source_id="src_1"))
|
|
assert result.source is src
|
|
assert result.notebook_id == "nb_1"
|
|
assert result.source_id == "src_1"
|
|
client.sources.get_or_none.assert_awaited_once_with("nb_1", "src_1")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_returns_none_when_missing() -> None:
|
|
client = _client()
|
|
client.sources.get_or_none = AsyncMock(return_value=None)
|
|
result = await execute_source_get(client, SourceGetPlan(notebook_id="nb_1", source_id="gone"))
|
|
assert result.source is None
|
|
|
|
|
|
# ===========================================================================
|
|
# execute_source_fulltext
|
|
# ===========================================================================
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fulltext_threads_output_format() -> None:
|
|
client = _client()
|
|
ft = SourceFulltext(source_id="src_1", title="One", content="body", char_count=4)
|
|
client.sources.get_fulltext = AsyncMock(return_value=ft)
|
|
result = await execute_source_fulltext(
|
|
client, SourceFulltextPlan(notebook_id="nb_1", source_id="src_1", output_format="markdown")
|
|
)
|
|
assert result.fulltext is ft
|
|
client.sources.get_fulltext.assert_awaited_once_with("nb_1", "src_1", output_format="markdown")
|
|
|
|
|
|
# ===========================================================================
|
|
# execute_source_guide — summary + keyword normalisation
|
|
# ===========================================================================
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guide_normalises_keywords() -> None:
|
|
client = _client()
|
|
client.sources.get_guide = AsyncMock(
|
|
return_value=SourceGuide(summary="A summary", keywords=(" alpha ", "beta", "", " "))
|
|
)
|
|
result = await execute_source_guide(
|
|
client, SourceGuidePlan(notebook_id="nb_1", source_id="src_1")
|
|
)
|
|
assert result.summary == "A summary"
|
|
# Stripped + blank-filtered.
|
|
assert result.keywords == ("alpha", "beta")
|
|
assert result.is_empty is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guide_empty_summary_and_keywords_is_empty() -> None:
|
|
client = _client()
|
|
client.sources.get_guide = AsyncMock(return_value=SourceGuide(summary=" ", keywords=()))
|
|
result = await execute_source_guide(
|
|
client, SourceGuidePlan(notebook_id="nb_1", source_id="src_1")
|
|
)
|
|
assert result.summary == " "
|
|
assert result.keywords == ()
|
|
assert result.is_empty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guide_non_str_summary_coerced_to_empty() -> None:
|
|
client = _client()
|
|
# Defensive: a malformed backend payload with a non-str summary.
|
|
guide = MagicMock()
|
|
guide.summary = None
|
|
guide.keywords = ["kw"]
|
|
client.sources.get_guide = AsyncMock(return_value=guide)
|
|
result = await execute_source_guide(
|
|
client, SourceGuidePlan(notebook_id="nb_1", source_id="src_1")
|
|
)
|
|
assert result.summary == ""
|
|
assert result.keywords == ("kw",)
|
|
# Summary empty but keywords present → not empty.
|
|
assert result.is_empty is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guide_non_iterable_keywords_coerced_to_empty() -> None:
|
|
client = _client()
|
|
guide = MagicMock()
|
|
guide.summary = "S"
|
|
guide.keywords = None
|
|
client.sources.get_guide = AsyncMock(return_value=guide)
|
|
result = await execute_source_guide(
|
|
client, SourceGuidePlan(notebook_id="nb_1", source_id="src_1")
|
|
)
|
|
assert result.keywords == ()
|
|
|
|
|
|
def test_guide_result_is_empty_property_summary_only() -> None:
|
|
# A summary alone is enough to be non-empty.
|
|
assert SourceGuideResult(source_id="s", summary="x", keywords=()).is_empty is False
|
|
|
|
|
|
# ===========================================================================
|
|
# execute_source_stale — freshness predicate
|
|
# ===========================================================================
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stale_fresh_source() -> None:
|
|
client = _client()
|
|
client.sources.check_freshness = AsyncMock(return_value=True)
|
|
result = await execute_source_stale(
|
|
client, SourceStalePlan(notebook_id="nb_1", source_id="src_1")
|
|
)
|
|
assert result.is_fresh is True
|
|
assert result.stale is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stale_outdated_source() -> None:
|
|
client = _client()
|
|
client.sources.check_freshness = AsyncMock(return_value=False)
|
|
result = await execute_source_stale(
|
|
client, SourceStalePlan(notebook_id="nb_1", source_id="src_1")
|
|
)
|
|
assert result.is_fresh is False
|
|
assert result.stale is True
|
|
|
|
|
|
def test_stale_result_stale_is_inverse_of_is_fresh() -> None:
|
|
assert SourceStaleResult(notebook_id="n", source_id="s", is_fresh=True).stale is False
|
|
assert SourceStaleResult(notebook_id="n", source_id="s", is_fresh=False).stale is True
|