Files
teng-lin--notebooklm-py/tests/_guardrails/test_no_cli_client_patch_surface.py
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

88 lines
4.1 KiB
Python

"""Recurrence gate: no ``*_cmd`` module exposes a patchable ``NotebookLMClient``.
#1481 replaced the per-command-module ``NotebookLMClient`` monkeypatch seam with
client-factory injection through Click's ``ctx.obj``. Command bodies now build the
client via ``cli.auth_runtime.resolve_client_factory(ctx)``; tests inject a fake
through ``runner.invoke(..., obj=inject_client(mock_client))`` (see
``tests/unit/cli/conftest.py``). The old ``patch("notebooklm.cli.X_cmd.NotebookLMClient")``
seam — across ~564 sites — is retired.
This gate locks that shut: every ``notebooklm.cli.*_cmd`` module must NOT expose
``NotebookLMClient`` as a module attribute. A direct ``from ..client import
NotebookLMClient`` (or any re-introduction of the inline ``async with
NotebookLMClient(...)`` construction) would re-create the patch surface this
refactor removed, and copy-paste from an old example is the likely regression
vector — so the gate is enforced, not documented.
Why a runtime ``hasattr`` check (not a test-file string scan): the failure mode is
"a command module re-exposes the client name", observable directly on the live
module surface. ``hasattr`` after import catches every rebind form — plain import,
``patch.object``, ``monkeypatch.setattr``, ``__getattr__`` forward — where a
string scan of tests would miss ``patch.object``/``setattr``/f-string forms and
would false-positive on the *legitimate* ``patch("notebooklm.client.NotebookLMClient")``
source-module seam (which is NOT a ``*_cmd`` attribute and stays valid).
Modules that annotate with the type keep it under ``if TYPE_CHECKING:`` — that
import never executes at runtime, so it creates no module attribute and passes
this gate (the four ``artifact``/``note``/``download``/``session`` modules do this).
"""
from __future__ import annotations
import importlib
from pathlib import Path
import pytest
import notebooklm.cli as _cli_pkg
_CLI_DIR = Path(_cli_pkg.__file__).resolve().parent
def _command_modules() -> list[str]:
"""Every ``notebooklm.cli.*_cmd`` module, discovered from disk.
Dynamic discovery (not a hardcoded list) so a *future* command module that
re-introduces the old seam is caught automatically.
"""
return sorted(p.stem for p in _CLI_DIR.glob("*_cmd.py"))
def test_discovery_found_the_command_modules() -> None:
"""Guard against the glob silently matching nothing (which would make the
parametrized gate vacuously pass)."""
mods = _command_modules()
assert len(mods) >= 10, f"expected the full *_cmd module set, found {mods}"
@pytest.mark.parametrize("mod_name", _command_modules())
def test_cmd_module_has_no_client_attribute(mod_name: str) -> None:
"""No ``*_cmd`` module may expose a runtime ``NotebookLMClient`` attribute."""
module = importlib.import_module(f"notebooklm.cli.{mod_name}")
assert not hasattr(module, "NotebookLMClient"), (
f"notebooklm.cli.{mod_name}.NotebookLMClient reappeared as a module "
"attribute. The per-command client patch seam was retired in #1481 — "
"command bodies resolve the client via "
"cli.auth_runtime.resolve_client_factory(ctx) and tests inject a fake via "
"runner.invoke(..., obj=inject_client(mock_client)). Do NOT add "
"`from ..client import NotebookLMClient` at module scope or re-introduce "
"`async with NotebookLMClient(...)`; if the type is only needed for an "
"annotation, import it under `if TYPE_CHECKING:`."
)
def test_injection_seam_exists() -> None:
"""Sanity: the replacement seam (``resolve_client_factory``) is present, so
the gate above is asserting a real migration, not an empty surface."""
from notebooklm.cli.auth_runtime import resolve_client_factory
assert callable(resolve_client_factory)
def test_source_module_client_seam_is_untouched() -> None:
"""The legitimate source-module seam stays valid: ``notebooklm.client``
still exposes ``NotebookLMClient`` (used by ``patch("notebooklm.client.NotebookLMClient")``
in contract tests). This gate must never push tests away from it."""
client_mod = importlib.import_module("notebooklm.client")
assert hasattr(client_mod, "NotebookLMClient")