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
182 lines
6.5 KiB
Python
182 lines
6.5 KiB
Python
"""Strict-mode coverage for ``NotebooksAPI.get_summary``.
|
|
|
|
The site at ``_notebooks.py:get_summary`` used to swallow ``IndexError`` /
|
|
``TypeError`` from an unguarded ``result[0][0][0]`` descent. It was migrated
|
|
to ``safe_index`` so drift raises ``UnknownRPCMethodError`` carrying
|
|
``method_id=RPCMethod.SUMMARIZE.value`` for debuggability. As of #1485 the
|
|
descent is delegated to the shared ``_extract_summary`` helper (single source
|
|
of truth with ``get_description``), so drift now surfaces with
|
|
``source='_notebooks._extract_summary'`` and the genuinely-absent shapes
|
|
(``result[0]`` None / empty / null summary slot) return ``""`` identically in
|
|
both entry points. Strict decoding is the only mode — the legacy
|
|
``NOTEBOOKLM_STRICT_DECODE=0`` soft-mode opt-out (which warn-logged and
|
|
returned ``""``) was retired in v0.7.0; see ADR-0011.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from notebooklm._notebooks import NotebooksAPI
|
|
from notebooklm.exceptions import UnknownRPCMethodError
|
|
from notebooklm.rpc import RPCMethod
|
|
|
|
|
|
def _make_api(rpc_return):
|
|
from tests._fixtures.fake_core import make_fake_core
|
|
|
|
api = NotebooksAPI.__new__(NotebooksAPI)
|
|
core = make_fake_core(rpc_call=AsyncMock(return_value=rpc_return))
|
|
api._rpc = core
|
|
return api
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_happy_path_returns_string():
|
|
"""Well-formed response shape extracts the summary string."""
|
|
# Real shape: [[[summary_string, ...], topics, ...]]
|
|
api = _make_api([[["the summary text"]]])
|
|
|
|
summary = await api.get_summary("nb_happy")
|
|
|
|
assert summary == "the summary text"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_drift_raises_typed_error():
|
|
"""Present-but-malformed payload raises ``UnknownRPCMethodError`` (the only mode)."""
|
|
# result = [[[]]]: result[0] (== outer) is [[]] — present and non-None — but
|
|
# its summary slot result[0][0] is an empty list, so the safe_index descent
|
|
# into result[0][0][0] raises IndexError. A present-but-malformed inner
|
|
# payload is genuine drift (distinct from a routinely-absent/None result[0]).
|
|
api = _make_api([[[]]])
|
|
|
|
with pytest.raises(UnknownRPCMethodError) as exc_info:
|
|
await api.get_summary("nb_drift")
|
|
|
|
err = exc_info.value
|
|
assert err.method_id == RPCMethod.SUMMARIZE.value
|
|
# The descent is delegated to _extract_summary, so the drift carries the
|
|
# shared helper's source label (#1485).
|
|
assert err.source == "_notebooks._extract_summary"
|
|
assert err.data_at_failure is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_scalar_result_zero_raises():
|
|
"""A scalar ``result[0]`` is present-but-malformed drift, not absence.
|
|
|
|
Regression for #1485 codex review: the server returned ``result == [123]``,
|
|
so ``result[0]`` is a bare int rather than the expected
|
|
``[summary_string, ...]`` list. This must raise drift — a
|
|
``not isinstance(..., list)`` short-circuit would wrongly suppress it to "".
|
|
"""
|
|
api = _make_api([123])
|
|
|
|
with pytest.raises(UnknownRPCMethodError) as exc_info:
|
|
await api.get_summary("nb_scalar_drift")
|
|
|
|
err = exc_info.value
|
|
assert err.method_id == RPCMethod.SUMMARIZE.value
|
|
assert err.source == "_notebooks._extract_summary"
|
|
# The very first descent hop (result[0][0]) fails on the scalar, so the
|
|
# truncated path is empty.
|
|
assert err.path == ()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_str_result_zero_raises():
|
|
"""A *string* ``result[0]`` raises drift, not a 1-char "summary".
|
|
|
|
Regression for #1485 codex review (second round): the server returned
|
|
``result == ["abc"]``, so ``result[0]`` is a bare string. A string is
|
|
indexable, so a naive descent would return ``"a"`` (``"abc"[0]``) and
|
|
silently pass it off as the summary. safe_index now rejects intermediate
|
|
str/bytes, so this surfaces as drift.
|
|
"""
|
|
api = _make_api(["abc"])
|
|
|
|
with pytest.raises(UnknownRPCMethodError) as exc_info:
|
|
await api.get_summary("nb_str_drift")
|
|
|
|
assert exc_info.value.source == "_notebooks._extract_summary"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_falsy_summary_returns_empty():
|
|
"""A None/empty summary at the expected path returns ``""``.
|
|
|
|
Distinguishes "drift" (shape mismatch, which raises) from "empty value"
|
|
(valid shape, nothing to surface) — the latter descends successfully to
|
|
``None`` and returns ``""``.
|
|
"""
|
|
api = _make_api([[[None]]])
|
|
|
|
summary = await api.get_summary("nb_empty_value")
|
|
|
|
assert summary == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_empty_notebook_returns_empty():
|
|
"""A summary-less notebook (result[0] is None) returns "" not drift.
|
|
|
|
Regression for #1485: a brand-new, source-less notebook has no summary
|
|
yet, so the SUMMARIZE result[0] payload is None. That routine "no summary
|
|
yet" state must surface as "" rather than ``UnknownRPCMethodError``.
|
|
"""
|
|
api = _make_api([None])
|
|
|
|
summary = await api.get_summary("nb_empty_notebook")
|
|
|
|
assert summary == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_empty_result_list_returns_empty():
|
|
"""An empty/absent outer result returns "" rather than raising.
|
|
|
|
Regression for #1485: result being [] (no outer[0] slot at all) is the
|
|
same routine "no summary yet" state and must not be mis-classified as
|
|
wire-schema drift.
|
|
"""
|
|
api = _make_api([])
|
|
|
|
summary = await api.get_summary("nb_empty_result")
|
|
|
|
assert summary == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_null_summary_slot_returns_empty():
|
|
"""A null summary slot (result[0][0] is None) returns "" — consistency win.
|
|
|
|
Regression for #1485 codex review: ``result == [[None]]`` means the
|
|
``outer`` payload is present but its summary slot is explicitly null. Before
|
|
delegating to ``_extract_summary``, ``get_summary`` raised drift here while
|
|
``get_description`` returned "" for the same shape. Both now agree on "".
|
|
"""
|
|
api = _make_api([[None]])
|
|
|
|
summary = await api.get_summary("nb_null_slot")
|
|
|
|
assert summary == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_summary_empty_outer_returns_empty():
|
|
"""An empty ``outer`` (result == [[]]) returns "" — consistency win.
|
|
|
|
Regression for #1485 codex review: ``result == [[]]`` means ``result[0]``
|
|
is an empty list (no summary slot at all) — the same routine "no summary
|
|
yet" absence that ``_extract_summary`` already treated as "". ``get_summary``
|
|
now delegates and agrees, rather than raising drift on an empty container.
|
|
"""
|
|
api = _make_api([[]])
|
|
|
|
summary = await api.get_summary("nb_empty_outer")
|
|
|
|
assert summary == ""
|