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

122 lines
5.2 KiB
Python

"""Maintainer helper: record the REST-server artifact cassettes against live API.
The server adapter issues some RPCs with a *different request shape* than the CLI
(e.g. the CLI resolves all sources first while the server passes ``source_ids``
through verbatim), so the existing CLI cassettes don't match the server path.
This script records server-shaped cassettes for the artifact lifecycle endpoints
that ``tests/server/test_integration_real_client.py`` then replays.
It drives the **server** through FastAPI's ``TestClient`` with a real
``NotebookLMClient`` (auth from your ``~/.notebooklm`` profile), under VCR record
mode, so the recorded ``f.req`` shapes are exactly what the server emits.
Records (into ``tests/cassettes``):
* ``server_generate_quiz.yaml`` — POST artifacts (quiz, all sources) → 202 + poll
* ``server_download_mind_map.yaml`` — POST artifacts/download (mind-map) → bytes
* ``server_add_file.yaml`` — POST sources/file (multipart upload) → 201
Usage (maintainer, one Google account with a populated generation notebook)::
NOTEBOOKLM_GENERATION_NOTEBOOK_ID=<uuid> \
uv run python tests/scripts/record_server_cassettes.py
The notebook must own at least one source (for generation) and one completed
mind-map (for the download leg). After recording, verify the cassettes are clean:
uv run python tests/scripts/check_cassettes_clean.py --strict --recursive
CI never runs this script. Replay uses scrubbed cassettes + mock auth.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# Make the repo root importable so ``tests.vcr_config`` resolves when this script
# is run directly (pytest sets pythonpath=["."]; a plain ``python`` run does not).
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
# Record mode + keepalive-poke disable MUST be set before importing the VCR
# config (its record_mode is computed at import time) and before any client opens
# (so the RotateCookies poke never records an extra interaction).
os.environ["NOTEBOOKLM_VCR_RECORD"] = "1"
os.environ["NOTEBOOKLM_DISABLE_KEEPALIVE_POKE"] = "1"
import io # noqa: E402
from contextlib import asynccontextmanager # noqa: E402
from fastapi.testclient import TestClient # noqa: E402
from tests.vcr_config import notebooklm_vcr # noqa: E402
from notebooklm.auth import AuthTokens # noqa: E402
from notebooklm.client import NotebookLMClient # noqa: E402
from notebooklm.server.app import create_app # noqa: E402
_TOKEN = "record-token" # nosec - local only; never leaves this process
#: Content for the recorded file-upload source. Enough text to be a valid source.
_UPLOAD_BYTES = (
b"NotebookLM REST server VCR upload fixture. This file exercises the multipart "
b"upload path: spool to a server-owned temp file, then INIT_UPLOAD + PUT bytes "
b"+ ADD_SOURCE through the real client.\n"
)
def _notebook_id() -> str:
nb = os.environ.get("NOTEBOOKLM_GENERATION_NOTEBOOK_ID", "").strip()
if not nb:
sys.exit("Set NOTEBOOKLM_GENERATION_NOTEBOOK_ID=<uuid> (a notebook with sources).")
return nb
def _fresh_client() -> TestClient:
"""A TestClient over a fresh real-client app (a TestClient is single-entry)."""
@asynccontextmanager
async def factory():
auth = await AuthTokens.from_storage() # real profile auth
async with NotebookLMClient(auth) as client:
yield client
os.environ["NOTEBOOKLM_SERVER_TOKEN"] = _TOKEN
app = create_app(client_factory=factory)
headers = {"Authorization": f"Bearer {_TOKEN}", "Host": "127.0.0.1"}
return TestClient(app, headers=headers, raise_server_exceptions=False)
def main() -> int:
nb = _notebook_id()
# Generate over ALL sources (no source_ids) — the server now defaults to all
# sources like the CLI, so a bare generate no longer 502s "… unavailable".
print("Recording server_generate_quiz.yaml (generate quiz, all sources + one poll)...")
with notebooklm_vcr.use_cassette("server_generate_quiz.yaml"), _fresh_client() as c:
gen = c.post(f"/v1/notebooks/{nb}/artifacts", json={"type": "quiz"})
print(" generate ->", gen.status_code, str(gen.json())[:120])
task_id = gen.json().get("task_id")
if task_id:
poll = c.get(f"/v1/notebooks/{nb}/artifacts/{task_id}")
print(" poll ->", poll.status_code, str(poll.json())[:120])
print("Recording server_download_mind_map.yaml (download completed mind-map)...")
with notebooklm_vcr.use_cassette("server_download_mind_map.yaml"), _fresh_client() as c:
dl = c.post(f"/v1/notebooks/{nb}/artifacts/download", json={"type": "mind-map"})
print(" download ->", dl.status_code, f"{len(dl.content)} bytes")
print("Recording server_add_file.yaml (multipart file upload)...")
with notebooklm_vcr.use_cassette("server_add_file.yaml"), _fresh_client() as c:
files = {"file": ("server-vcr-upload.txt", io.BytesIO(_UPLOAD_BYTES), "text/plain")}
up = c.post(f"/v1/notebooks/{nb}/sources/file", files=files)
print(" upload ->", up.status_code, str(up.json())[:120])
print(
"Done. Now verify: uv run python tests/scripts/check_cassettes_clean.py --strict --recursive"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())