Files
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

218 lines
8.3 KiB
Python

"""Patch-surface compatibility tests for ``notebooklm.cli.helpers``.
After P2.T1 consolidates the canonical resolver implementations into
``notebooklm.cli.resolve``, the ``notebooklm.cli.helpers`` module continues
to expose ``require_notebook``, ``validate_id``, ``_resolve_partial_id``,
``resolve_notebook_id``, etc. as a **compatibility facade**. These names are
patch targets in many existing tests; the facade must survive renames or
shape changes in the canonical implementation.
This test file is the explicit pin for that contract. Critical guarantees:
1. Patching ``helpers.require_notebook`` intercepts calls made through the
helpers module namespace.
2. Patching ``helpers.get_context_path`` still affects
``helpers.require_notebook``'s context-file-fallback behavior. ~20
existing tests in ``test_helpers.py`` rely on this.
3. Patching ``helpers.console`` still affects
``helpers.require_notebook``'s no-notebook diagnostic. Several existing
tests rely on this.
4. ``helpers.validate_id`` and ``helpers._resolve_partial_id`` remain
importable and call-compatible with the canonical resolver.
If this file ever fails, the patch surface has regressed and downstream
test files will start breaking - fix the facade, not this test.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import click
import pytest
from notebooklm.cli import helpers
# ----------------------------------------------------------------------------
# 1. Patching helpers.require_notebook intercepts CLI calls
# ----------------------------------------------------------------------------
class TestMonkeypatchOnHelpersRequireNotebook:
"""A patch of ``helpers.require_notebook`` must intercept
``helpers.require_notebook(...)`` calls.
This is the explicit acceptance criterion from P2.T1.
"""
def test_monkeypatch_setattr_intercepts_direct_call(self, monkeypatch):
sentinel = "INTERCEPTED_NOTEBOOK_ID"
def fake_require_notebook(notebook_id):
return sentinel
monkeypatch.setattr(helpers, "require_notebook", fake_require_notebook)
result = helpers.require_notebook("ignored")
assert result == sentinel
def test_monkeypatch_undone_after_test(self):
"""Sanity: after monkeypatch undoes itself, the real function returns.
This guards against a bad facade implementation that would cache the
patched function in a closure and leak between tests.
"""
# The previous test patched the symbol. After fixture teardown,
# calling with a real id should still go through the normal path.
# Without a context file, env var, or arg, it raises SystemExit.
with pytest.raises(SystemExit):
helpers.require_notebook(None)
# ----------------------------------------------------------------------------
# 2. patching helpers.get_context_path still steers helpers.require_notebook
# ----------------------------------------------------------------------------
class TestHelpersGetContextPathPatchSurface:
"""Patching ``helpers.get_context_path`` must affect
``helpers.require_notebook`` so the ~20 existing tests in
``test_helpers.py`` that do this keep passing.
"""
def test_context_file_path_patch_steers_resolution(self, tmp_path):
"""An empty/non-existent context path leads to SystemExit."""
with (
patch.object(
helpers,
"get_context_path",
return_value=tmp_path / "nonexistent.json",
),
pytest.raises(SystemExit),
):
helpers.require_notebook(None)
def test_context_file_path_patch_returns_context_value(self, tmp_path):
"""A patched ``get_context_path`` pointing at a real file lets the
context-fallback rung return its notebook id."""
context_file = tmp_path / "context.json"
context_file.write_text('{"notebook_id": "nb_from_context"}')
with patch.object(helpers, "get_context_path", return_value=context_file):
assert helpers.require_notebook(None) == "nb_from_context"
# ----------------------------------------------------------------------------
# 3. patching helpers.console still steers the error-path output
# ----------------------------------------------------------------------------
class TestHelpersConsolePatchSurface:
def test_console_patch_captures_no_notebook_diagnostic(self, tmp_path):
"""Patching ``helpers.console`` captures the no-notebook error
message that ``require_notebook`` prints before raising
``SystemExit``."""
with (
patch.object(
helpers,
"get_context_path",
return_value=tmp_path / "nonexistent.json",
),
patch.object(helpers, "console") as mock_console,
):
with pytest.raises(SystemExit):
helpers.require_notebook(None)
mock_console.print.assert_called_once()
printed = mock_console.print.call_args[0][0]
# User-facing flag is named.
assert "-n/--notebook" in printed
# ----------------------------------------------------------------------------
# 4. validate_id + _resolve_partial_id remain importable from helpers
# ----------------------------------------------------------------------------
class TestResolverFacadeReExports:
def test_resolver_names_are_canonical_aliases(self):
from notebooklm.cli import resolve
assert helpers.require_notebook is resolve.require_notebook
assert helpers.validate_id is resolve.validate_id
assert helpers._resolve_partial_id is resolve._resolve_partial_id
assert helpers.resolve_notebook_id is resolve.resolve_notebook_id
assert helpers.resolve_source_id is resolve.resolve_source_id
assert helpers.resolve_artifact_id is resolve.resolve_artifact_id
def test_validate_id_importable_from_helpers(self):
from notebooklm.cli.helpers import validate_id
# Trivial smoke check that the facade name is callable.
assert validate_id(" abc ") == "abc"
def test_validate_id_facade_matches_canonical(self):
from notebooklm.cli import resolve
# Same function or same behavior across multiple inputs.
for case in ("a", " spaced ", "abc12345-6789-4abc-def0-1234567890ab"):
assert helpers.validate_id(case) == resolve.validate_id(case)
def test_validate_id_facade_raises_same_exception(self):
with pytest.raises(click.ClickException):
helpers.validate_id(" ")
@pytest.mark.asyncio
async def test_resolve_partial_id_importable_from_helpers(self):
from notebooklm.cli.helpers import _resolve_partial_id
class Item:
def __init__(self, id_, title):
self.id = id_
self.title = title
async def list_fn():
return [Item("aaa111", "First"), Item("bbb222", "Second")]
result = await _resolve_partial_id("aaa", list_fn, "thing", "thing list")
assert result == "aaa111"
@pytest.mark.asyncio
async def test_resolve_notebook_id_importable_from_helpers(self):
from notebooklm.cli.helpers import resolve_notebook_id
client = MagicMock()
class Notebook:
def __init__(self, id_, title):
self.id = id_
self.title = title
client.notebooks = MagicMock()
client.notebooks.list = AsyncMock(
return_value=[Notebook("nb_111", "First"), Notebook("nb_222", "Second")]
)
result = await resolve_notebook_id(client, "nb_1")
assert result == "nb_111"
# ----------------------------------------------------------------------------
# 5. helpers.* names remain present in cli/__init__.py public surface
# ----------------------------------------------------------------------------
class TestPublicSurfaceRemainsStable:
"""The ``cli/__init__.py`` public re-exports must not lose
``require_notebook``, ``resolve_notebook_id``, etc."""
def test_public_names_present(self):
from notebooklm import cli
for name in (
"require_notebook",
"resolve_notebook_id",
"resolve_source_id",
"resolve_artifact_id",
):
assert hasattr(cli, name), f"cli public surface lost {name!r}"