)
notebooklm-py 的可再生测试基线derive/store/compare/regen 模式与一条命令的再生机制ADR-0022【免费下载链接】notebooklm-pyUnofficial Python API and agentic skill for Google Gemini Notebook. Full programmatic access to NotebookLMs features—including capabilities the web UI doesnt expose—via Python, CLI, and AI agents like Claude Code, Codex, and OpenClaw.项目地址: https://gitcode.com/GitHub_Trending/no/notebooklm-py本篇基于 ADR-0022Regenerable test baselines展开讲清 notebooklm-py 如何把「守卫测试中冻结的快照值」从手写字面量升级为可再生基线regenerable baselines先理解「derive / store / compare / regen」四段式模式的来龙去脉再看基线注册表Baseline数据类、参数化冻结测试与--update-baselines再生缝隙的具体实现最后覆盖 shrink-only 棘轮的--allow-growth显式确认机制与 CI「永不再生」不变量。读完你能掌握一套完整的「快照型守卫 一键再生 diff 即确认」的工程方案并能将其迁移到自己项目的测试守卫设计中。背景为什么要把「代码已能推导的值」冻结成快照notebooklm-py 的测试守卫guardrail中有一类特殊测试它们冻结的是代码本身已经会计算出来的值的快照。这样做的目的是让公共 API 表面的任何变化成为一个「有意的、在 diff 中可见」的动作而不是悄无声息地生长。ADR 在 Context 一节指出了关键的不对称性scripts/audit_public_api_compat.py这个兼容性审计只对照上一个 release tag 标记被移除/变更的导出对新增的公共符号是盲的——所以新增符号必须靠另一类「快照守卫」来逼出一次人工确认。被这类守卫冻结的值包括均出自 ADR 原文列举notebooklm.types.__all__—— 有文档、有顺序的公共类型表面每个「未加锁」公共模块notebooklm、notebooklm.config、notebooklm.exceptions等的收集后公共表面__all__ 可解析的 allowlist 额外项;公共 CLI 的命令树、选项、帮助文本与别名模块大小上限以及「锁不可用lock-unavailable策略的所有者」——两者都是 shrink-only只允许收紧、不允许放宽的棘轮ratchet其冻结值从活代码树推导而来auth 导入图与 auth 测试打补丁点位patch-site清单。历史做法手写字面量以及它的代价这些值曾经被冻结为测试模块里手敲的字面量——_FROZEN_TYPES_ALL与_UNGATED_PUBLIC_ALL_SNAPSHOT都位于 tests/_guardrails/test_public_surface_manifest.py。每一份都是测试本来就知道如何推导的值的精确副本list(notebooklm.types.__all__)、_collected_public_surface(module)。于是每加一个公共符号就得手工编辑若干份冻结字面量去匹配代码每次运行都会重新算出来的值——边际成本高而且是典型的复制错误高发区。仓库里已经有一条守卫做对了这件事tests/unit/cli/test_cli_contract.py会推导build_cli_contract()把结果提交到 tests/fixtures/cli_contract_baseline.json断言两者相等并自带一个__main__打印器用于重新生成该文件。这个模式——derive / store / compare / regen推导 / 存储 / 比对 / 再生——具备内联字面量所没有的性质再生只需一条命令而提交的工件本身就是那次被评审过的确认动作。决策核心一个基线注册表 一条再生命令ADR-0022 的决策是把 CLI 契约的特例泛化成单一的基线注册表baseline registry并配一条干净的再生命令。落地实现见 tests/_baselines/registry.pydataclass(frozenTrue) class Baseline: name: str path: Path derive: Callable[[], object] sort_keys: bool False growth_check: GrowthCheck | None field(defaultNone, compareFalse) # Extra metadata kept out of equality/hash; documents intent. description: str field(default, compareFalse) def dump(self, value: object) - str: Serialize value to the committed-on-disk JSON string (trailing newline). return json.dumps(value, indent2, sort_keysself.sort_keys) \n def load(self) - object: The committed baseline value (parsed JSON). return json.loads(self.path.read_text(encodingutf-8)) def write(self, *, allow_growth: bool False) - None: Rewrite the committed file from derive(). Dev-only (regen seam). 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). ) derived self.derive() if self.growth_check is not None and self.path.is_file() and not allow_growth: growth self.growth_check(self.load(), derived) if growth: details \n .join(growth) raise RuntimeError( frefusing to grow the shrink-only {self.name} baseline:\n {details}\n Review the growth, then rerun python scripts/regen_baselines.py --allow-growth to acknowledge it explicitly. ) self.path.parent.mkdir(parentsTrue, exist_okTrue) self.path.write_text(self.dump(derived), encodingutf-8)设计要点逐条对应 ADR 的 Decision 章节注册表。Baseline是 frozen dataclass(name, path, derive, sort_keys, …)描述一份可再生基线BASELINES列表是所有条目的索引。最关键的一条纪律是每个derive可调用对象复用已有的计算函数绝不复制字面量。例如types_all→list(notebooklm.types.__all__)保持顺序导出顺序本身有意义ungated_surface→ 对UNGATED_PUBLIC_MODULES逐模块执行collect_public_surface(module)有序列表cli_contract→build_cli_contract()dictsort_keysTruemodule_size→ 全局预算加上超预算豁免与 ADR-0033 shrink 锁的实测值见后文storage_transaction_policy→ 从 AST 直接推导出的三个「锁不可用策略」的调用者auth_import_graph/auth_patch_sites及其浏览器侧姊妹项browser_import_graph/browser_patch_sites→ 各审计脚本的投影patch 投影使用 schema-v2 的完整联表行另有auth_facade_patch_sites、auth_family_patch_scorecard、auth_shared_mutations把门面/包迁移与共享属主位移暴露出来——这五个变更类工件全部是 shrink-onlyguardrail_inline_literals→tests/_guardrails/下「大型模块级容器字面量」的祖父清单grandfathered inventory。提交的 JSON 与序列化规范。基线文件位于 tests/fixtures/baselines/如types_all.json、ungated_surface.jsoncli_contract保留其原有路径 tests/fixtures/cli_contract_baseline.json 并把它登记为Baseline.path。序列化规则值得注意有序列表按原顺序写出列表永远不排序sort_keys只影响 dict 的键序Baseline.dump中即json.dumps(value, indent2, sort_keysself.sort_keys) \n。这样types_all这类「顺序即语义」的表面不会被规范化操作破坏。一个冻结测试。test_baseline_matches_committed_file对BASELINES参数化加载提交文件、断言其等于derive()并且提交的字节必须与规范序列化形式一致——保证在新鲜检出上再生是一个 no-op幂等且能抓住「碰巧解析后相等、但格式不同」的手改。再生缝隙regen seam。一个 dev-only 的--update-baselinespytest 选项定义在 tests/conftest.py把冻结测试从断言翻转成把derive()写入pathscripts/regen_baselines.py 是可发现的封装器内部 shell 出pytest … --update-baselines。shrink-only 的增长确认。注册表条目可携带growth_check(previous, current)回调普通再生接受收紧与移除但拒绝上限提高、新增策略调用者、新增或增大的内联守卫字面量评审之后必须由开发者显式执行python scripts/regen_baselines.py --allow-growth来确认。CI 则同时拒绝两个再生标志。从当前注册表源码看BASELINES已扩展到16 条基线registry.py 的 BASELINES 列表在 ADR 原始列举之外还包含三个backend_*条目backend_runtime_coupling、backend_static_coupling、backend_boundary——这本身就验证了 ADR 的动机新模式下新增一条基线只是往BASELINES里加一个Baseline(...)条目无需再发明一套 derive/store/compare 管道。一个容易混淆的边界被 ADR 与代码同时划清auth 行为场景、覆盖率损失豁免、生命周期清理与最终幸存者台账是「作者策略」authored policies不是可再生基线——它们位于 tests/fixtures/policies/auth_behavior_scenarios.json、auth_coverage_allowances.json、auth_lifecycle_cleanup.json、auth_patch_survivors.json。其校验器拒绝缺失、多余、过期、通配符行--allow-growth既不能写入它们也不能为它们背书。同理module_size基线中 JSON 存的是实测上限而「哪些路径豁免、为什么豁免」的评审性策略源仍是 tests/_baselines/module_size.py。derive如何「复用而不复制」两个典型实现以ungated_surface为例注册表提供的共享推导原语registry.pylru_cache(maxsize1) def allowlist_extra_public_names() - dict[str, list[str]]: 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]: 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 namesallowlist 额外项直接走scripts/audit_public_api_compat.py自己的load_policy含同一套 schema 校验、大小写不敏感去重排序策略文件是 scripts/api-compat-allowlist.json——守卫与审计共用同一解析契约天然不可能漂移。UNGATED_PUBLIC_MODULES元组列出 15 个模块notebooklm、notebooklm.artifacts、notebooklm.config…notebooklm.utils即审计发现的全部公共模块减去四个在别处被__all__精确钉死的模块notebooklm.auth/client/rpc/types。storage_transaction_policy则是纯静态分析tests/_baselines/storage_transaction_policy.py用ast解析src/notebooklm/_auth/profile_store.py与storage.py为三个策略函数raise_on_lock_unavailable、report_on_lock_unavailable、skip_on_lock_unavailable分别收集直接调用者清单storage_transaction_policy_growth随后对新增调用者逐条产出增长描述。冻结测试一个参数化测试钉住所有基线冻结逻辑集中在 tests/_guardrails/test_public_surface_manifest.py。核心断言如下def test_baseline_matches_committed_file( baseline: Baseline, update_baselines: bool, allow_baseline_growth: bool, ) - None: if update_baselines: baseline.write(allow_growthallow_baseline_growth) return assert baseline.path.is_file(), ( fcommitted baseline {baseline.path} is missing — regenerate with python scripts/regen_baselines.py ) with warnings.catch_warnings(): warnings.simplefilter(ignore, DeprecationWarning) derived baseline.derive() committed baseline.load() assert derived committed, ( f{baseline.name} baseline ({baseline.path.name}) is stale. If the change is intentional, regenerate it in this PR (python scripts/regen_baselines.py) — that diff is the deliberate acknowledgement. ) # The committed bytes must be exactly what write() would emit, so a # regen is a no-op on a fresh checkout (idempotency) ... assert baseline.dump(committed) baseline.path.read_text(encodingutf-8), ( f{baseline.name} baseline is not in canonical serialized form; regenerate it (python scripts/regen_baselines.py). )注意失败信息本身的设计stale 断言的报错直接告诉开发者再生命令——ADR 在 Consequences 中把这视为对新流程的补偿「多了一个 regen 步骤」这一 unwanted 后果由失败测试点名命令来消化。另外还有一个防「空注册表骗过参数化测试」的守卫测试test_baseline_registry_is_non_trivial同文件 L1311-L1337它把全部 16 个稳定基线名固定为断言集合的子集并要求名字唯一——若有人清空BASELINES参数化冻结测试会空洞地通过而这个测试会大声报出。参数化条目还对重活条目打了超时标记auth_family_patch_scorecard与backend_runtime_coupling360 秒四个 patch-site 投影 180 秒其余无额外超时。再生缝隙--update-baselines与「dev-only 再生」不变量再生路径的全部接线在 tests/conftest.pydef pytest_addoption(parser): parser.addoption( --update-baselines, actionstore_true, defaultFalse, help( DEV ONLY: rewrite committed baseline fixtures from live code instead of asserting against them. CI must never pass this (it only diffs). Prefer python scripts/regen_baselines.py. ), ) parser.addoption( --allow-growth, actionstore_true, defaultFalse, help( DEV ONLY: explicitly acknowledge growth in shrink-only baselines. Valid only together with --update-baselines. ), ) pytest.fixture def update_baselines(request) - bool: requested bool(request.config.getoption(--update-baselines)) if requested and os.environ.get(CI, ).strip(): raise pytest.UsageError( --update-baselines must not be used in CI: baselines are dev-only regenerated and CI only diffs (ADR-0022). Unset CI or drop the flag. ) return requested def pytest_configure(config): allow_growth bool(config.getoption(--allow-growth)) if allow_growth and not config.getoption(--update-baselines): raise pytest.UsageError(--allow-growth requires --update-baselines) if allow_growth and os.environ.get(CI, ).strip(): raise pytest.UsageError( --allow-growth must not be used in CI: growth acknowledgement is a local, reviewed baseline-regeneration action (ADR-0022). )Dev-only-regen 不变量是 ADR 中最强调的一条再生只发生在开发者传入--update-baselines时CI 永远不传这个标志——CI 只做 diff。而且这不是「写在文档里」而是被强制执行的——事实上是三层强制update_baselinesfixture检测到CI环境变量时抛pytest.UsageErrorpytest_configure--allow-growth缺--update-baselines直接报错且两个标志在 CI 下都会被拒Baseline.write()在缝隙本体上再查一次CI上文代码即使标志被绕过接线也会拒绝重写封装器 scripts/regen_baselines.py 在入口处检测CI打印refusing to regenerate baselines in CI: CI only diffs (ADR-0022).并以退出码 2 返回def main(argv: list[str] | None None) - int: argv list(sys.argv[1:] if argv is None else argv) if os.environ.get(CI, ).strip(): print( refusing to regenerate baselines in CI: CI only diffs (ADR-0022). Run this locally and commit the result., filesys.stderr, ) return 2 cmd [ sys.executable, -m, pytest, _BASELINE_FREEZE_TEST, --update-baselines, -q, -p, no:cacheprovider, *argv, ] ...该脚本只运行那一个冻结测试tests/_guardrails/test_public_surface_manifest.py::test_baseline_matches_committed_file成功后提示「reviewgit diff— each change is a deliberate acknowledgement」。docs/development.md 也把这套再生工作流写进了贡献者文档python scripts/regen_baselines.py与--allow-growth两条命令。shrink-only 棘轮--allow-growth与模块大小案例「棘轮」指一类只能朝收紧方向转的约束module_size与storage_transaction_policy都属于此类。growth_check回调定义了「什么算需要背书的增长」普通再生命令自动接受收紧/移除遇到增长则抛出带明细的RuntimeError要求重跑python scripts/regen_baselines.py --allow-growth。tests/_baselines/module_size.py 是最佳案例它清晰展示了「策略 vs 状态」的分离MODULE_SIZE_BUDGET 1500 OVER_BUDGET_EXEMPTIONS: dict[str, str] { _android/proto/google/internal/labs/tailwind/orchestration/v1/ orchestration_service_pb2_grpc.py: ( deterministic protoc output for the complete generated Android service; ... ), exceptions.py: ( canonical public exception home; moving classes would fork their documented provenance ), _android/sources.py: ( the complete native source surface for one backend; ... ), } SHRINK_LOCKED_MODULES: tuple[str, ...] ( _browser/browser_capture.py, _auth/psidts_recovery.py, _auth/refresh.py, _auth/storage.py, )derive_module_size()同文件 L46-L95从源码树实测每个模块行数返回{budget, allowlisted_ceilings, shrink_locked_ceilings}并附带多重防御豁免必须带持久的文字理由缺理由直接RuntimeError策略路径在源码树中消失会报 stale已经回落到预算内的豁免被视为过期要求先删除作者条目再再生任何超过预算却没有豁免或 shrink 锁的模块直接让推导失败。提交的 tests/fixtures/baselines/module_size.json 因此存的是实测上限而module_size_growth把「预算提高、上限提高」逐条列为增长。ADR 对模块大小还有一条特别重要的纪律再生不替开发者创作豁免。一个超过全局预算的模块必须先被加入OVER_BUDGET_EXEMPTIONS带持久理由ADR-0033 的 shrink 锁留在SHRINK_LOCKED_MODULES中移除一个活的 shrink 锁本身就被算作增长所以--allow-growth无法把一个受保护模块悄悄变回普通模块。明确不纳入的范围Scope boundaryADR 用一整节划出「哪些东西故意不再生」这三条边界对复用此模式时避免误用很有参考价值**_DOCUMENTED_PUBLIC_IMPORTS保持手工维护。**它编码的是作者意图承诺的导入表面不是推导出的事实把它再生化会让对应的测试变成同义反复tautological。**_TOP_LEVEL_TYPE_EXPORTS保持作者化Phase 2 候选。**朴素的推导谓词——「notebooklm.__all__中对象is同一个notebooklm.types属性的名字」——会过度收集会把经由notebooklm.types转手的异常与 mind-map 再导出也拉进来因此不是干净的 derive。迁移需要更锋利的谓词已推迟。Phase 2推迟tests/fixtures/rpc_golden/*与json_stdout基线的「可推导一半」只支持逐字段的手术式再生不在本 ADR 范围内。后果与备选方案的取舍ADR 的 Consequences 一节值得完整继承因为它解释了这套模式真正的价值所在想要的**一条命令再生。**加一个公共符号 跑python scripts/regen_baselines.py 评审 diff而不是编辑手敲字面量**无复制漂移。**提交文件与守卫测试来自同一个函数不可能因手误而不一致**棘轮保持方向性。**一条命令的再生无法悄悄批准模块上限或守卫快照债务的增长——那些变更有独立的显式标志因而有一个醒目的评审 diff**同样的 diff 可见性。**表面新增仍然产生一行被评审过的 diff——有意的确认动作被保留了下来只是换成了 JSON 载体。不想要的**多了一个 regen 步骤。**表面变更现在要求跑再生命令而不是直接编辑——但失败的冻结测试会把命令名直接报给开发者**间接层。**冻结值住在 JSON fixture 里而不是测试源码里注册表是 name ↔ derive ↔ file 的索引。被否决的备选保留手写字面量每个符号的编辑成本与复制错误风险正是本 ADR 要消除的摩擦失配即自动接受不提交文件放弃了 diff 可见的确认——「静默的表面生长」正是这些守卫要防的失效模式每个基线一套专属再生器CLI 契约的现状不可扩展每新增一条基线都要重新发明 derive/store/compare 管道和自己的再生入口。实践工作流小结把上述机制串成日常操作就是三句话你改了公共表面新增导出、新增 CLI 命令、模块超限等后跑python scripts/regen_baselines.py遇到 shrink-only 增长上限提高、新策略调用者、新大型内联字面量、解除 shrink 锁先评审然后显式确认python scripts/regen_baselines.py --allow-growth提交前评审git diff——每一行变更都是对一次公共表面变更的刻意确认CI 侧永远不会再生只做derive()与提交文件的比对外加「提交字节必须等于规范序列化形式」这一幂等性检查新鲜检出上再生是 no-op。对types_all这类顺序敏感的表面提交文件如 tests/fixtures/baselines/types_all.json 以ArtifactDownloadListing、ArtifactDownloadRequest… 开头的有序列表就是导出顺序的评审记录本身。整体来看ADR-0022 提供的不仅是一个测试技巧而是一种「守卫与再生共用同一推导路径、确认动作外化为可评审 diff、放宽必须显式背书、自动化只比对不写入」的完整治理模式——这也是注册表能从 ADR 写就时的若干条基线平滑扩张到今天 16 条、且零新增管道代码的原因。【免费下载链接】notebooklm-pyUnofficial Python API and agentic skill for Google Gemini Notebook. Full programmatic access to NotebookLMs features—including capabilities the web UI doesnt expose—via Python, CLI, and AI agents like Claude Code, Codex, and OpenClaw.项目地址: https://gitcode.com/GitHub_Trending/no/notebooklm-py创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考