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
217 lines
8.3 KiB
Python
217 lines
8.3 KiB
Python
"""The regenerable-baseline registry (ADR-0022).
|
|
|
|
One place that knows, per *regenerable baseline*, how to:
|
|
|
|
* **derive** it from live code (``Baseline.derive``);
|
|
* where its committed JSON file lives (``Baseline.path``);
|
|
* how to **serialize** it deterministically (``Baseline.dump`` /
|
|
``Baseline.sort_keys``).
|
|
|
|
A baseline is a value the code already derives — e.g. ``notebooklm.types.__all__``
|
|
or the collected public surface of the ungated public modules. The freeze tests
|
|
in ``tests/_guardrails/test_public_surface_manifest.py`` load the committed file
|
|
and assert it equals ``derive()``; ``scripts/regen_baselines.py`` (via the
|
|
``--update-baselines`` pytest flag) rewrites the file from ``derive()``.
|
|
|
|
**Dev-only-regen invariant.** Regeneration only ever happens when a developer
|
|
passes ``--update-baselines`` to pytest. CI never passes the flag, so CI only
|
|
ever *diffs* derive() against the committed file. See ADR-0022.
|
|
|
|
The derive callables reuse the production-facing surface (``notebooklm`` imports)
|
|
and the audit's own ``load_policy`` — they never copy values. Adding one public
|
|
symbol then becomes a one-command regen instead of hand-editing snapshot literals.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import os
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
# ``tests/`` directory (this file is tests/_baselines/registry.py).
|
|
_TESTS_ROOT = Path(__file__).resolve().parents[1]
|
|
_PROJECT_ROOT = _TESTS_ROOT.parent
|
|
_FIXTURES_DIR = _TESTS_ROOT / "fixtures"
|
|
_BASELINES_DIR = _FIXTURES_DIR / "baselines"
|
|
|
|
# Audit source-of-truth for the allowlist ``extra_public_names`` (mirrors
|
|
# ``scripts/audit_public_api_compat.py``). The collected public surface for a
|
|
# module is ``__all__`` plus any *resolvable* allowlist extras not already in it.
|
|
_ALLOWLIST_PATH = _PROJECT_ROOT / "scripts" / "api-compat-allowlist.json"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared derivation primitives (also imported by the freeze tests so the gate
|
|
# and the regen path derive identically — no copy).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def allowlist_extra_public_names() -> dict[str, list[str]]:
|
|
"""Allowlist ``extra_public_names`` via the audit's OWN ``load_policy`` — the
|
|
same schema validation + case-insensitive sort/dedupe the audit applies, so
|
|
this can't drift from the audit's contract, parsed once. Lazy import to keep
|
|
the audit module (and its collector machinery) off the registry-import path.
|
|
"""
|
|
import scripts.audit_public_api_compat as audit
|
|
|
|
_allowances, extras = audit.load_policy(_ALLOWLIST_PATH)
|
|
return extras
|
|
|
|
|
|
def collect_public_surface(module_name: str) -> list[str]:
|
|
"""The audit-collected export surface for ``module_name``: ``__all__`` plus
|
|
any *resolvable* ``extra_public_names`` not already in ``__all__`` — mirroring
|
|
``scripts/audit_public_api_compat.py::collect_module``. Order is ``__all__``
|
|
(its own order) first, then the normalized extras. A non-resolving name *in*
|
|
``__all__`` is kept (unlike the audit, which re-raises) — that bad state is
|
|
caught independently by ``test_public_top_level_module_declares_all``.
|
|
"""
|
|
module = importlib.import_module(module_name)
|
|
names = list(getattr(module, "__all__", []))
|
|
for name in allowlist_extra_public_names().get(module_name, []):
|
|
if name not in names and hasattr(module, name):
|
|
names.append(name)
|
|
return names
|
|
|
|
|
|
# Ungated public modules whose collected surface is frozen by the
|
|
# ``ungated_surface`` baseline. These are every audit-discovered public module
|
|
# EXCEPT the four exact-``__all__``-pinned-elsewhere modules
|
|
# (``notebooklm.auth`` / ``client`` / ``rpc`` / ``types``). The exact set is a
|
|
# property pinned by ``test_ungated_public_surface_covers_exactly_the_unpinned_modules``;
|
|
# this list is the regen seed and is asserted complete against discovery there.
|
|
UNGATED_PUBLIC_MODULES: tuple[str, ...] = (
|
|
"notebooklm",
|
|
"notebooklm.artifacts",
|
|
"notebooklm.config",
|
|
"notebooklm.exceptions",
|
|
"notebooklm.io",
|
|
"notebooklm.log",
|
|
"notebooklm.migration",
|
|
"notebooklm.paths",
|
|
"notebooklm.research",
|
|
"notebooklm.urls",
|
|
"notebooklm.utils",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Derive callables (one per baseline). Each REUSES existing production surface;
|
|
# none copies a literal.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _derive_types_all() -> list[str]:
|
|
"""``notebooklm.types.__all__`` as an ordered list (export order is meaningful)."""
|
|
import notebooklm.types as public_types
|
|
|
|
return list(public_types.__all__)
|
|
|
|
|
|
def _derive_ungated_surface() -> dict[str, list[str]]:
|
|
"""The collected public surface of each ungated public module (ordered lists)."""
|
|
return {module: collect_public_surface(module) for module in UNGATED_PUBLIC_MODULES}
|
|
|
|
|
|
def _derive_cli_contract() -> dict[str, object]:
|
|
"""The deterministic public CLI inventory (``build_cli_contract``)."""
|
|
from tests._baselines.cli_contract import build_cli_contract
|
|
|
|
return build_cli_contract()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Baseline registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Baseline:
|
|
"""One regenerable baseline: derive it, locate its committed JSON, compare.
|
|
|
|
``derive`` returns the live value; ``path`` is the committed JSON file;
|
|
``sort_keys`` controls JSON key ordering on dump (lists always preserve
|
|
order — only dict *keys* are affected). ``load()`` reads the committed value;
|
|
``write()`` rewrites it from ``derive()`` (dev-only, behind
|
|
``--update-baselines``).
|
|
"""
|
|
|
|
name: str
|
|
path: Path
|
|
derive: Callable[[], object]
|
|
sort_keys: bool = False
|
|
# Extra metadata kept out of equality/hash; documents intent.
|
|
description: str = field(default="", compare=False)
|
|
|
|
def dump(self, value: object) -> str:
|
|
"""Serialize ``value`` to the committed-on-disk JSON string (trailing newline)."""
|
|
return json.dumps(value, indent=2, sort_keys=self.sort_keys) + "\n"
|
|
|
|
def load(self) -> object:
|
|
"""The committed baseline value (parsed JSON)."""
|
|
return json.loads(self.path.read_text(encoding="utf-8"))
|
|
|
|
def write(self) -> None:
|
|
"""Rewrite the committed file from ``derive()``. Dev-only (regen seam).
|
|
|
|
Enforces the dev-only-regen invariant at the seam itself (not only at the
|
|
``--update-baselines`` call site): a CI environment must never rewrite a
|
|
baseline. CI only ever diffs (ADR-0022).
|
|
"""
|
|
if os.environ.get("CI", "").strip():
|
|
raise RuntimeError(
|
|
"refusing to regenerate baselines in CI: baselines are dev-only "
|
|
"regenerated and CI only diffs (ADR-0022)."
|
|
)
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.path.write_text(self.dump(self.derive()), encoding="utf-8")
|
|
|
|
|
|
BASELINES: list[Baseline] = [
|
|
Baseline(
|
|
name="types_all",
|
|
path=_BASELINES_DIR / "types_all.json",
|
|
derive=_derive_types_all,
|
|
sort_keys=False,
|
|
description="notebooklm.types.__all__ (ordered export surface).",
|
|
),
|
|
Baseline(
|
|
name="ungated_surface",
|
|
path=_BASELINES_DIR / "ungated_surface.json",
|
|
derive=_derive_ungated_surface,
|
|
sort_keys=False,
|
|
description="Collected public surface of every ungated public module.",
|
|
),
|
|
Baseline(
|
|
name="cli_contract",
|
|
# Pre-existing path kept in place (the CLI contract test already uses it).
|
|
path=_FIXTURES_DIR / "cli_contract_baseline.json",
|
|
derive=_derive_cli_contract,
|
|
sort_keys=True,
|
|
description="Public CLI command tree, options, help, and aliases.",
|
|
),
|
|
]
|
|
|
|
|
|
def baseline_by_name(name: str) -> Baseline:
|
|
"""Look up a registered baseline by ``name`` (raises ``KeyError`` if absent)."""
|
|
for baseline in BASELINES:
|
|
if baseline.name == name:
|
|
return baseline
|
|
raise KeyError(f"no registered baseline named {name!r}")
|
|
|
|
|
|
__all__ = [
|
|
"BASELINES",
|
|
"Baseline",
|
|
"UNGATED_PUBLIC_MODULES",
|
|
"allowlist_extra_public_names",
|
|
"baseline_by_name",
|
|
"collect_public_surface",
|
|
]
|