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
77 lines
3.4 KiB
Python
77 lines
3.4 KiB
Python
"""Unit tests for the transport-neutral source batch-add policy (``_app.source_batch``).
|
|
|
|
``batch_item_is_fatal`` decides whether a per-item add failure aborts the batch. It
|
|
is the shared classifier both the REST route and the MCP tool consult, so a
|
|
mis-bucketed category would silently change REST's already-green batch behavior with
|
|
no other failing test — hence this exhaustive all-14-category gate with an
|
|
INDEPENDENT expected table (not a re-import of ``_FATAL_CATEGORIES``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from notebooklm import exceptions as exc
|
|
from notebooklm._app import SourceMutationError
|
|
from notebooklm._app.errors import ErrorCategory
|
|
from notebooklm._app.source_batch import MAX_BATCH_URLS, batch_item_is_fatal
|
|
|
|
# One exemplar exception per ErrorCategory (mirrors the classify-consistency gate).
|
|
_EXEMPLARS: list[tuple[ErrorCategory, BaseException]] = [
|
|
(ErrorCategory.NOT_FOUND, exc.SourceNotFoundError("src_456")),
|
|
(ErrorCategory.AUTH, exc.AuthError("auth failed")),
|
|
(ErrorCategory.RATE_LIMITED, exc.RateLimitError("slow down", retry_after=5)),
|
|
(ErrorCategory.VALIDATION, exc.ValidationError("bad input")),
|
|
(ErrorCategory.CONFIG, exc.ConfigurationError("missing config")),
|
|
(ErrorCategory.NETWORK, exc.NetworkError("connection refused")),
|
|
(ErrorCategory.NOTEBOOK_LIMIT, exc.NotebookLimitError(499, limit=500)),
|
|
(ErrorCategory.ARTIFACT_TIMEOUT, exc.ArtifactTimeoutError("nb-1", "task-1", 30.0)),
|
|
(ErrorCategory.TIMEOUT, exc.WaitTimeoutError("generic wait timed out")),
|
|
(ErrorCategory.SERVER, exc.ServerError("upstream 503")),
|
|
(ErrorCategory.RPC, exc.RPCError("decode failed", method_id="abc123")),
|
|
(ErrorCategory.SOURCE_MUTATION, SourceMutationError("ambiguous", "AMBIGUOUS_ID")),
|
|
(ErrorCategory.LIBRARY, exc.NotebookLMError("some library error")),
|
|
(ErrorCategory.UNEXPECTED, RuntimeError("boom")),
|
|
]
|
|
|
|
# INDEPENDENT oracle: fatal = the categories whose REST HTTP status is 401 / 429 /
|
|
# >=500 (a service/infra failure, not specific to one URL). Hand-written on purpose —
|
|
# do NOT derive this from `_FATAL_CATEGORIES` (that would make the test tautological).
|
|
_EXPECTED_FATAL: dict[ErrorCategory, bool] = {
|
|
ErrorCategory.NOT_FOUND: False, # 404
|
|
ErrorCategory.AUTH: True, # 401
|
|
ErrorCategory.RATE_LIMITED: True, # 429
|
|
ErrorCategory.VALIDATION: False, # 400
|
|
ErrorCategory.CONFIG: True, # 500
|
|
ErrorCategory.NETWORK: True, # 502
|
|
ErrorCategory.NOTEBOOK_LIMIT: False, # 409
|
|
ErrorCategory.ARTIFACT_TIMEOUT: True, # 504
|
|
ErrorCategory.TIMEOUT: True, # 504
|
|
ErrorCategory.SERVER: True, # 502
|
|
ErrorCategory.RPC: True, # 502
|
|
ErrorCategory.SOURCE_MUTATION: False, # 422
|
|
ErrorCategory.LIBRARY: True, # 500
|
|
ErrorCategory.UNEXPECTED: True, # 500
|
|
}
|
|
|
|
|
|
def test_expected_table_covers_every_category() -> None:
|
|
"""A new ErrorCategory without an expected verdict fails here (no silent gap)."""
|
|
assert set(_EXPECTED_FATAL) == set(ErrorCategory)
|
|
assert {category for category, _ in _EXEMPLARS} == set(ErrorCategory)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("category", "exception"),
|
|
_EXEMPLARS,
|
|
ids=[category.name for category, _ in _EXEMPLARS],
|
|
)
|
|
def test_batch_item_is_fatal_matches_expected(
|
|
category: ErrorCategory, exception: BaseException
|
|
) -> None:
|
|
assert batch_item_is_fatal(exception) is _EXPECTED_FATAL[category]
|
|
|
|
|
|
def test_max_batch_urls_is_positive() -> None:
|
|
assert isinstance(MAX_BATCH_URLS, int) and MAX_BATCH_URLS > 0
|