项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

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,
)