teng-lin--notebooklm-py
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
276 行
11 KiB
Python
276 行
11 KiB
Python
"""Behavioural tests for the public shim modules.
|
|
|
|
The static surface contracts — the documented public-import manifest, the
|
|
re-export identity pins, the frozen ``__all__`` ordering, the removed-name
|
|
guards, the facade-delegates-via-reflection checks, and the auth first-party
|
|
seam manifest — moved to ``tests/_guardrails/test_public_surface_manifest.py``
|
|
as part of the test-guardrail consolidation. What remains here are the
|
|
functions that *exercise runtime behaviour* through the public surface: the
|
|
``select_cited_sources`` / ``ResearchAPI`` back-compat delegations, the
|
|
``UnknownTypeWarning`` filter behaviour, and the ``NotebookLMClient.rpc_call``
|
|
kwarg-forwarding path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import warnings
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.repo_lint
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# notebooklm.research back-compat behaviour
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_research_select_cited_sources_returns_public_dataclass():
|
|
"""select_cited_sources returns the public CitedSourceSelection dataclass."""
|
|
from notebooklm.research import select_cited_sources
|
|
from notebooklm.types import CitedSourceSelection
|
|
|
|
result = select_cited_sources([], "")
|
|
assert isinstance(result, CitedSourceSelection)
|
|
assert result.used_fallback is True
|
|
|
|
|
|
def test_research_api_backward_compat_classmethod_delegates():
|
|
"""notebooklm._research.ResearchAPI.select_cited_sources still works."""
|
|
from notebooklm._research import ResearchAPI
|
|
from notebooklm.types import CitedSourceSelection
|
|
|
|
result = ResearchAPI.select_cited_sources([], "")
|
|
assert isinstance(result, CitedSourceSelection)
|
|
|
|
|
|
def test_research_api_extract_report_urls_backward_compat_classmethod_delegates(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""notebooklm._research.ResearchAPI.extract_report_urls still works."""
|
|
import notebooklm.research as research_module
|
|
from notebooklm._research import ResearchAPI
|
|
|
|
report = "See [Example](https://Example.com/path/)."
|
|
sentinel = {"delegated"}
|
|
calls: list[str] = []
|
|
|
|
def fake_extract_report_urls(value: str) -> set[str]:
|
|
calls.append(value)
|
|
return sentinel
|
|
|
|
monkeypatch.setattr(research_module, "extract_report_urls", fake_extract_report_urls)
|
|
|
|
assert ResearchAPI.extract_report_urls(report) is sentinel
|
|
assert calls == [report]
|
|
|
|
|
|
def test_research_api_reexports_cited_source_selection_for_back_compat():
|
|
"""notebooklm._research.CitedSourceSelection continues to resolve."""
|
|
from notebooklm._research import CitedSourceSelection as Legacy
|
|
from notebooklm.types import CitedSourceSelection
|
|
|
|
assert Legacy is CitedSourceSelection
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# UnknownTypeWarning filter / parser dedup behaviour
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_types_private_state_seams_are_live_objects(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Warning-dedup sets and compat map must remain shared between canonical
|
|
owners in ``_types/`` and their ``notebooklm.types`` re-exports."""
|
|
import notebooklm.types as public_types
|
|
from notebooklm._types import artifacts as _artifact_types_seam
|
|
from notebooklm._types import sources as _source_types_seam
|
|
from notebooklm.types import (
|
|
_SOURCE_TYPE_COMPAT_MAP,
|
|
Artifact,
|
|
ArtifactType,
|
|
Source,
|
|
SourceType,
|
|
UnknownTypeWarning,
|
|
)
|
|
|
|
assert _SOURCE_TYPE_COMPAT_MAP is public_types._SOURCE_TYPE_COMPAT_MAP
|
|
source_warnings: set[int] = set()
|
|
artifact_warnings: set[tuple[int | None, int | None]] = set()
|
|
# ADR-0007 seam-aliased object-target form: patch the canonical owners in
|
|
# ``_types/{sources,artifacts}`` directly. ``notebooklm.types._warned_*``
|
|
# are re-exports of those canonical objects, so the test must target the
|
|
# canonical home — patching the public facade would rebind only the
|
|
# facade alias and not the module-global the parser reads.
|
|
monkeypatch.setattr(_source_types_seam, "_warned_source_types", source_warnings)
|
|
monkeypatch.setattr(_artifact_types_seam, "_warned_artifact_types", artifact_warnings)
|
|
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", UnknownTypeWarning)
|
|
assert Source(id="source", _type_code=7654321).kind is SourceType.UNKNOWN
|
|
assert (
|
|
Artifact(id="artifact", title="Artifact", _artifact_type=7654322, status=3).kind
|
|
is ArtifactType.UNKNOWN
|
|
)
|
|
|
|
assert 7654321 in source_warnings
|
|
assert (7654322, None) in artifact_warnings
|
|
|
|
|
|
def test_facade_unknown_type_warning_filter_suppresses_parser_warnings() -> None:
|
|
"""Filters using notebooklm.types.UnknownTypeWarning still catch parser warnings."""
|
|
import notebooklm.types as public_types
|
|
|
|
public_types._warned_source_types.clear()
|
|
public_types._warned_artifact_types.clear()
|
|
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
warnings.filterwarnings("ignore", category=public_types.UnknownTypeWarning)
|
|
|
|
assert (
|
|
public_types.Source(id="source", _type_code=8765432).kind
|
|
is public_types.SourceType.UNKNOWN
|
|
)
|
|
assert (
|
|
public_types.Artifact(
|
|
id="artifact",
|
|
title="Artifact",
|
|
_artifact_type=8765433,
|
|
status=3,
|
|
).kind
|
|
is public_types.ArtifactType.UNKNOWN
|
|
)
|
|
|
|
assert caught == []
|
|
assert 8765432 in public_types._warned_source_types
|
|
assert (8765433, None) in public_types._warned_artifact_types
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# auth cookie-validation behaviour
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_auth_validation_uses_cookie_policy_rebindings_directly(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Validation rebindings must target the canonical home in ``_auth.cookie_policy``.
|
|
|
|
Wave 4 T2.2 of the session-decoupling plan removed the auth.py
|
|
write-through that previously copy-forwarded facade-level rebinds into
|
|
``_cookie_policy``. Tests that want to rebind policy names patch the
|
|
canonical module directly.
|
|
"""
|
|
from notebooklm import auth
|
|
from notebooklm._auth import cookie_policy
|
|
|
|
monkeypatch.setattr(cookie_policy, "MINIMUM_REQUIRED_COOKIES", {"SID"})
|
|
monkeypatch.setattr(cookie_policy, "_has_valid_secondary_binding", lambda names: True)
|
|
|
|
# ``auth._validate_required_cookies`` is the same object as
|
|
# ``cookie_policy._validate_required_cookies`` (see the identity gate in
|
|
# ``tests/_guardrails/test_public_surface_manifest.py``), so calling
|
|
# either reaches the canonical implementation which observes the rebind.
|
|
auth._validate_required_cookies({"SID"})
|
|
|
|
|
|
def test_auth_validation_extraction_hint_lives_on_cookie_policy(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The validator's error message uses ``_cookie_policy._EXTRACTION_HINT``."""
|
|
from notebooklm import auth
|
|
from notebooklm._auth import cookie_policy
|
|
|
|
monkeypatch.setattr(cookie_policy, "MINIMUM_REQUIRED_COOKIES", {"SID", "SIDTS"})
|
|
monkeypatch.setattr(cookie_policy, "_EXTRACTION_HINT", "custom extraction hint")
|
|
|
|
with pytest.raises(ValueError, match="custom extraction hint"):
|
|
auth._validate_required_cookies({"SID"})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# NotebookLMClient.rpc_call kwarg forwarding
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_client_rpc_call_forwards_supported_kwargs() -> None:
|
|
"""NotebookLMClient.rpc_call forwards its supported kwargs to the executor.
|
|
|
|
After the v0.6.0 cut, the public wrapper exposes only the supported
|
|
surface (``method``, ``params``, ``allow_null``, and the keyword-only
|
|
``disable_internal_retries``); the previously-deprecated
|
|
``source_path`` / ``_is_retry`` / ``operation_variant`` kwargs were
|
|
removed and are no longer forwarded by this layer.
|
|
"""
|
|
from notebooklm import NotebookLMClient
|
|
from notebooklm.auth import AuthTokens
|
|
from notebooklm.rpc import RPCMethod
|
|
from tests._fixtures.fake_core import make_fake_core
|
|
|
|
client = NotebookLMClient(
|
|
AuthTokens(
|
|
cookies={"SID": "test"},
|
|
csrf_token="csrf",
|
|
session_id="session",
|
|
)
|
|
)
|
|
# ADR-0007 constructor injection: substitute the whole executor
|
|
# collaborator with the seam fixture's fake instead of mutating
|
|
# ``client._rpc_executor.rpc_call`` after the fact. The public
|
|
# ``rpc_call`` wrapper reads ``self._rpc_executor``, so swapping the
|
|
# executor exercises the same forwarding path.
|
|
fake = make_fake_core(rpc_call=AsyncMock(return_value={"ok": True}))
|
|
client._rpc_executor = fake.rpc_executor
|
|
|
|
result = await client.rpc_call(
|
|
RPCMethod.CREATE_NOTEBOOK,
|
|
["My Notebook"],
|
|
allow_null=True,
|
|
disable_internal_retries=True,
|
|
)
|
|
|
|
assert result == {"ok": True}
|
|
fake.rpc_executor.rpc_call.assert_awaited_once_with(
|
|
method=RPCMethod.CREATE_NOTEBOOK,
|
|
params=["My Notebook"],
|
|
allow_null=True,
|
|
disable_internal_retries=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_client_rpc_call_forwards_default_arguments() -> None:
|
|
"""The default-shape call forwards minimal kwargs and inherits executor defaults."""
|
|
from notebooklm import NotebookLMClient
|
|
from notebooklm.auth import AuthTokens
|
|
from notebooklm.rpc import RPCMethod
|
|
from tests._fixtures.fake_core import make_fake_core
|
|
|
|
client = NotebookLMClient(
|
|
AuthTokens(
|
|
cookies={"SID": "test"},
|
|
csrf_token="csrf",
|
|
session_id="session",
|
|
)
|
|
)
|
|
# No async context is needed: ADR-0007 constructor injection swaps the
|
|
# whole executor collaborator for the seam fixture's fake before any
|
|
# real transport initialization can be required.
|
|
fake = make_fake_core(rpc_call=AsyncMock(return_value=[]))
|
|
client._rpc_executor = fake.rpc_executor
|
|
|
|
result = await client.rpc_call(RPCMethod.LIST_NOTEBOOKS, [])
|
|
|
|
assert result == []
|
|
# The wrapper forwards only the kwargs it owns; the rest of
|
|
# RpcExecutor.rpc_call's signature (source_path, _is_retry,
|
|
# operation_variant) keeps its module-level defaults.
|
|
fake.rpc_executor.rpc_call.assert_awaited_once_with(
|
|
method=RPCMethod.LIST_NOTEBOOKS,
|
|
params=[],
|
|
allow_null=False,
|
|
disable_internal_retries=False,
|
|
)
|