Files
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

148 lines
6.1 KiB
Python

"""Schema-drift tests for ``_parse_generation_result``.
These tests pin down the strict-decoding contract:
* ``_parse_generation_result`` accepts ``method_id`` as a keyword argument and
threads it through ``safe_index`` so drift diagnostics know which RPC failed.
* Drift raises ``UnknownRPCMethodError`` carrying the supplied ``method_id``
so operators can detect that Google's response shape moved out from under us.
Strict decoding is the only mode — the legacy
``NOTEBOOKLM_STRICT_DECODE=0`` soft-mode opt-out (which returned the
``GenerationStatus(status="failed", task_id="")`` sentinel) was retired in
v0.7.0; see ADR-0011.
Real-shape happy-path coverage for the wire-level flow already exists in
``tests/integration/test_artifacts_integration.py::TestParseGenerationResult``
and elsewhere. Here we exercise the parser directly with constructed dicts
because the drift branch is a runtime concern that doesn't depend on HTTP
plumbing — a VCR cassette would only add ceremony without exercising it.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from notebooklm._artifacts import ArtifactsAPI
from notebooklm.exceptions import UnknownRPCMethodError
from notebooklm.rpc import RPCMethod
@pytest.fixture
def artifacts_api():
"""Build a minimal ArtifactsAPI for direct parser invocation."""
from notebooklm._mind_map import NoteBackedMindMapService
from notebooklm._note_service import NoteService
from tests._fixtures.fake_core import make_fake_core
mock_core = make_fake_core(rpc_call=AsyncMock())
return ArtifactsAPI(
rpc=mock_core,
drain=mock_core,
lifecycle=mock_core,
notebooks=MagicMock(),
mind_maps=MagicMock(spec=NoteBackedMindMapService),
note_service=MagicMock(spec=NoteService),
)
# ---------------------------------------------------------------------------
# Happy-path: real response shape parses correctly for both call sites.
# ---------------------------------------------------------------------------
class TestParseGenerationResultHappyPath:
"""Real response shape parses successfully when ``method_id`` is supplied."""
def test_create_artifact_real_shape(self, artifacts_api):
"""CREATE_ARTIFACT response: [[task_id, title, type_code, None, status]]."""
result = [["task_abc", "Audio Overview", 1, None, 1]]
status = artifacts_api._parse_generation_result(
result, method_id=RPCMethod.CREATE_ARTIFACT.value
)
assert status.task_id == "task_abc"
assert status.status == "in_progress"
assert status.error is None
def test_revise_slide_real_shape(self, artifacts_api):
"""REVISE_SLIDE shares the response shape with CREATE_ARTIFACT."""
result = [["revised_xyz", "Slide Deck", 8, None, 3]]
status = artifacts_api._parse_generation_result(
result, method_id=RPCMethod.REVISE_SLIDE.value
)
assert status.task_id == "revised_xyz"
assert status.status == "completed"
assert status.error is None
# ---------------------------------------------------------------------------
# Drift: strict decoding raises typed UnknownRPCMethodError (the only mode).
# ---------------------------------------------------------------------------
class TestParseGenerationResultStrictDrift:
"""Strict decoding raises typed errors on drift (the only mode)."""
def test_none_result_raises(self, artifacts_api):
with pytest.raises(UnknownRPCMethodError) as exc_info:
artifacts_api._parse_generation_result(None, method_id=RPCMethod.CREATE_ARTIFACT.value)
err = exc_info.value
assert err.method_id == RPCMethod.CREATE_ARTIFACT.value
assert err.source == "_parse_generation_result"
# Top-level descent: failing path is empty (we failed at the first index).
assert err.path == ()
def test_none_result_raises_for_revise_slide(self, artifacts_api):
with pytest.raises(UnknownRPCMethodError) as exc_info:
artifacts_api._parse_generation_result(None, method_id=RPCMethod.REVISE_SLIDE.value)
err = exc_info.value
assert err.method_id == RPCMethod.REVISE_SLIDE.value
assert err.source == "_parse_generation_result"
assert err.path == ()
def test_empty_list_raises_for_revise_slide(self, artifacts_api):
with pytest.raises(UnknownRPCMethodError) as exc_info:
artifacts_api._parse_generation_result([], method_id=RPCMethod.REVISE_SLIDE.value)
err = exc_info.value
assert err.method_id == RPCMethod.REVISE_SLIDE.value
assert err.source == "_parse_generation_result"
def test_inner_leaf_missing_raises(self, artifacts_api):
"""Drift on the inner leaf reports a non-empty failing path."""
with pytest.raises(UnknownRPCMethodError) as exc_info:
artifacts_api._parse_generation_result([[]], method_id=RPCMethod.CREATE_ARTIFACT.value)
err = exc_info.value
assert err.method_id == RPCMethod.CREATE_ARTIFACT.value
# We descended into result[0], then failed on result[0][0].
assert err.path == (0,)
def test_status_code_missing_raises(self, artifacts_api):
"""task_id present but status_code position absent must raise.
``status_code`` is treated as a required leaf: in every captured real
response it sits at ``result[0][4]``. If Google starts shipping a
truncated shape like ``[["task_short"]]`` (task_id only, no
status_code), we want to learn about it via a typed drift exception
rather than silently falling back to ``"pending"``.
"""
with pytest.raises(UnknownRPCMethodError) as exc_info:
artifacts_api._parse_generation_result(
[["task_short"]], method_id=RPCMethod.CREATE_ARTIFACT.value
)
err = exc_info.value
assert err.method_id == RPCMethod.CREATE_ARTIFACT.value
assert err.source == "_parse_generation_result"
# We descended into result[0] (a list of length 1), then failed on
# result[0][4] — so the failing path stops at (0,).
assert err.path == (0,)