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
218 lines
8.3 KiB
Python
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}"
|