Zotero CLI Harness 测试体系深度解析:从单元测试到真实 Zotero 的端到端验证
Zotero CLI Harness 测试体系深度解析:从单元测试到真实 Zotero 的端到端验证
cli-anything-zotero 是 CLI-Anything 生态中面向 Zotero 桌面端的 agent-native 命令行工具,它不重新实现 Zotero,而是组合其真实的本地能力面(SQLite 只读盘点、connector 写通道、Local API 检索与导出)。本文以 TEST.md 为骨架,结合 README.md 与各测试/核心模块源码,完整解析该 harness 的分层测试设计——包括测试清单、单元测试计划、CLI/子进程计划、Live E2E 计划、opt-in 写测试策略与真实验证结果。读完本文,你将掌握如何复现这套测试、理解每条命令背后的验证边界,以及如何在真实 Zotero 库上安全地做端到端验证。
一、测试架构总览:四个测试文件的职责划分
TEST.md 开篇即给出完整测试清单,整个测试套件按职责分为四个文件,覆盖从"纯函数级"到"真实桌面软件运行"的完整梯度:
| 测试文件 | 关注点 | 覆盖层次 |
|---|---|---|
| test_core.py | 路径发现、SQLite 检查、library-aware 引用解析、note/context/analyze 助手、实验性 SQLite 写入、导入核心 | 单元 + mock HTTP |
| test_cli_entrypoint.py | CLI help、REPL 入口、子进程行为、fake connector/Local API/OpenAI 流程、群组库路由 | 安装后/子进程行为 |
| test_agent_harness.py | 打包完整性、harness 结构、skill 生成 | 打包完整性 |
| test_full_e2e.py | 真实 Zotero 运行时、安全读流程、opt-in 写流程 | 真实环境验证 |
这种分层设计在源码结构中同样清晰可辨:tests/ 目录下四个测试文件与 core/(discovery、catalog、imports、notes、rendering、session、experimental、analysis)、utils/(zotero_paths、zotero_sqlite、zotero_http、openai_api)形成一一对应的验证面。测试助手集中在 _helpers.py,其中 create_sample_environment() 会现场构建一套完整的模拟 Zotero 目录树与 SQLite 数据库(含 user/group 两个库、收藏夹、条目、笔记、附件、标签、保存的搜索),保证单测可在无真实 Zotero 的环境下稳定运行。
二、单元测试计划详解
2.1 路径发现(Path Discovery)
路径发现是 harness 的"地基",对应 utils/zotero_paths.py 中的 build_environment(),测试覆盖:
- 从显式路径和环境变量解析 profile 根目录(
ZOTERO_PROFILE_DIR环境变量与显式--profile-dir参数均可生效); - 解析
profiles.ini(按Default=1标记的活动 profile 优先,见find_active_profile()); - 解析
prefs.js与user.js; - 解析自定义
extensions.zotero.dataDir偏好(对应源码中的DATA_DIR_PREF常量); - 回退到
~/Zotero默认数据目录; - 解析可执行文件与版本(源码通过安装目录下的
application.ini读取 Zotero 版本); - 检测 Local API 偏好状态(
extensions.zotero.httpServer.localAPI.enabled)。
在 test_core.py 的 PathDiscoveryTests 中,test_build_environment_falls_back_to_home_zotero 通过 mock Path.home() 验证了无 dataDir 偏好时的回退逻辑,而 test_ensure_local_api_enabled_writes_user_js 则验证了 ensure_local_api_enabled() 会向 profile 写入 Local API 开关。
2.2 SQLite 检查(SQLite Inspection)
SQLite 检查对应 utils/zotero_sqlite.py 中的 fetch_* / resolve_* 系列函数,测试覆盖:
- libraries(用户库 + 群组库枚举);
- collections 与嵌套 collection 树(
fetch_collections+build_collection_tree); - 标题搜索与收藏夹搜索(
find_collections_and_items_and_notes); - items、notes、attachments 与 annotations;
- item 的字段、创作者与标签(
resolve_item聚合 fields/creators/tags); - 保存的搜索(saved searches)及其条件;
- 按标签反向查条目;
- 附件真实路径解析(
resolve_attachment_real_path能区分 storage 型与 linked 型附件,后者直接返回file:///指向的绝对路径); - 跨用户库/群组库的重复键解析。
其中"重复键解析"是核心设计亮点:items_library_key 唯一索引允许同一个 key 出现在不同库中。resolve_item 在不提供 library_id 且 key 存在跨库重复时抛出 AmbiguousReferenceError,要求调用方先用 session use-library <id> 指定库上下文;而唯一 key 则无需上下文也能自动解析(见 test_core.py 的 test_duplicate_key_resolution_requires_library_context 与 test_cross_library_unique_key_still_resolves_without_session_context)。
2.3 上下文、笔记与分析(Context, Notes, and Analysis)
覆盖 item find 的 Local API 优先 + SQLite 回退双路径、群组库 Local API scope 选择(/api/groups/<libraryID>/...)、item notes 与 note get、note add 的 text/markdown 载荷构造(安全转义为 HTML)、item context 对链接/笔记/导出结果的聚合,以及 item analyze 的 OpenAI 请求路径与缺失 API key 时的报错路径。item analyze 的 OpenAI 调用封装在 utils/openai_api.py,测试通过 mock 或 fake 端点验证其 Responses API 请求与响应文本抽取,不依赖真实外部 API。
2.4 实验性 SQLite 写入(Experimental SQLite Writes)
对应 core/experimental.py,覆盖:
collection create;item add-to-collection(仅追加一条收藏夹成员关系);item move-to-collection(追加目标收藏夹并移除指定来源的成员关系);- 写入前自动备份;
- 单事务提交/回滚行为;
- Zotero 运行中守卫(Zotero 必须已关闭才允许写);
- 仅限本地用户库的限制。
这些命令被刻意标记为实验性(--experimental 必填),且绝不对真实 Zotero 库执行,只在隔离的 SQLite 副本上验证,见 test_core.py 的 test_experimental_commands_require_closed_zotero_and_update_db_copy。
2.5 导入核心(Import Core)
导入核心对应 core/imports.py,是测试密度最高的模块,覆盖:
app enable-local-api的幂等性(重复启用不报错);- 写命令的 connector 必启守卫(Zotero 未运行时导入直接失败);
import file的原始文本交接(RIS/BibTeX/BibLaTeX/Refer 文本直接 POST 给 connector);import json的解析与校验(非法 JSON 被拒绝);import json内联attachments数组的抽取与剥离(附件从元数据中移除、单独上传,非法内联附件 schema 被拒绝);--attachments-manifest解析与 index/title 校验(import file侧车清单,index 越界或 expected_title 不匹配都会记为附件失败);- 本地文件与 URL PDF 校验,包括
Content-Type错误时通过 magic-byte(PDF 文件头%PDF-)接受——fake 服务中专门提供了/downloads/wrong-content-type.pdf这类端点验证此逻辑; - 部分成功(partial success)附件上报与非零退出语义;
- 同一次导入请求内重复附件描述符的幂等跳过;
- session 目标回退链(显式
--collection→ 会话收藏夹 → GUI 当前选中收藏夹 → 用户库); - 可重复标签向
updateSession的传播(同一次会话中多条记录的 tag 都会追加到更新请求)。
三、CLI / 子进程测试计划
test_cli_entrypoint.py 以"安装后的真实命令行为"为验证目标,覆盖:
- 根命令
--help; - 默认 REPL 入口(无子命令时进入有状态 REPL);
- REPL help 文本(须包含内置命令提示);
app status --json/app enable-local-api --json;collection list/find --json;item get/find/notes/context --json;note get/add --json;- 针对 fake OpenAI 兼容端点的
item analyze --json; - 群组库路由:
item find/export/citation/bibliography/search items自动使用/api/groups/<libraryID>/...scope; session use-library L<id>归一化(session use-library 1与session use-library L1等价);CLI_ANYTHING_FORCE_INSTALLED=1强制已安装子进程解析(找不到真实命令时直接报错);- 带内联本地/URL PDF 附件的
import json; - 带
--attachments-manifest的import file; - 附件部分失败返回非零退出码;
- 针对隔离 SQLite 副本的实验性 collection 写入命令。
从源码看,子进程测试通过 test_full_e2e.py 的 resolve_cli() 决定调用方式:优先取 cli-anything-zotero 可执行文件,否则回退到 python -m cli_anything.zotero;CLI_ANYTHING_FORCE_INSTALLED=1 时则强制要求 PATH 中存在真实安装命令。
四、Live E2E 计划
test_full_e2e.py 依赖真实 Zotero 本地数据(zotero_sqlite.build_environment() 找不到数据目录时整个测试类被 skipUnless 跳过),并在 setUpClass 中自动 ensure_live_api_enabled()、按需 launch_zotero()。
4.1 非变异(Non-Mutating)流程
app ping(connector 存活检查);collection use-selected(读取正在运行的 Zotero 窗口中 GUI 当前选中项);collection tree/get/items;item list/get/find/attachments/file;item notes;note get;tag list/items;search list/get/items(当库中存在保存的搜索时);session use-collection/use-item;style list(读取数据目录下的已安装 CSL 样式);item context;item citation/item bibliography;item export --format ris|bibtex|csljson。
4.2 变异(Mutating)流程
import file;import json;- 带内联本地 PDF 附件的
import json; note add。
写测试默认不执行,仅当显式设置两个环境变量时才开启:
set CLI_ANYTHING_ZOTERO_ENABLE_WRITE_E2E=1
set CLI_ANYTHING_ZOTERO_IMPORT_TARGET=<collection-key-or-id>
而实验性 SQLite 写入命令则永远不对真实 Zotero 库执行,只在隔离的 SQLite 副本上验证——这是 TEST.md 明确的安全边界。
五、真实验证结果与运行环境
TEST.md 记录了一次于 2026-03-27 完成的完整验证,可作为这套测试"跑通"的可信参考:
5.1 机器 / 运行时
| 项 | 值 |
|---|---|
| OS | Windows |
| Python | 3.13.5 |
| Zotero 可执行文件 | C:\Program Files\Zotero\zotero.exe |
| Zotero 版本 | 7.0.32 |
| 活动 profile | C:\Users\Lenovo\AppData\Roaming\Zotero\Zotero\Profiles\38ay0ldk.default |
| 活动数据目录 | D:\Study\科研\论文 |
| HTTP 端口 | 23119 |
| Local API 状态 | 已启用且可用 |
5.2 验证命令
py -m pip install -e .
py -m pytest cli_anything/zotero/tests/test_core.py -v
py -m pytest cli_anything/zotero/tests/test_cli_entrypoint.py -v
py -m pytest cli_anything/zotero/tests/test_agent_harness.py -v
py -m pytest cli_anything/zotero/tests/test_full_e2e.py -v -s
py -m pytest cli_anything/zotero/tests/ -v --tb=no
$env:CLI_ANYTHING_FORCE_INSTALLED=1
py -m pytest cli_anything/zotero/tests/test_cli_entrypoint.py -v
py -m pytest cli_anything/zotero/tests/test_full_e2e.py -v -s
cli-anything-zotero --json app status
cli-anything-zotero --json collection find "具身"
cli-anything-zotero --json item find "embodied intelligence" --limit 5
cli-anything-zotero --json item context PB98EI9N --include-links
cli-anything-zotero --json note get <note-key>
注意:仓库中这些命令的工作目录为 zotero/agent-harness,安装命令对应 setup.py。若 cli-anything-zotero 未进入 PATH,可改用 py -m cli_anything.zotero --help(见 README.md)。
5.3 真实 Zotero 结果
app status --json报告connector_available: true、local_api_available: true、local_api_enabled_configured: true,与 core/discovery.py 中build_runtime_context()的探测逻辑一一对应;collection use-selected --json成功返回正在运行的 Zotero 窗口中的 GUI 实时选中项;item find通过 Local API 搜索路径在真实库条目上命中;item context在真实条目上产出了结构化元数据与 prompt-ready 文本;- 有子笔记的真实条目上
item notes与note get成功; - 真实常规条目上
item citation、item bibliography、item export全部成功; - 导出校验通过:RIS 含
TY -、BibTeX 含@、CSL JSON 可被json.loads解析。
5.4 写测试策略结果
import file、import json、导入期 PDF 附件、note add的 mocked connector 写路径测试全部通过;- 同一写路径的子进程测试(含内联与 manifest 附件流程)在 fake 本地服务上通过;
- mocked 群组库 Local API 路由(
item find、item export、item citation、item bibliography、search items)通过; - 带
CLI_ANYTHING_FORCE_INSTALLED=1的已安装命令子进程检查通过; - 真实写导入、真实带附件导入、真实 note add E2E 默认保持 opt-in 不执行;
- 实验性 SQLite 写入命令仅在隔离的本地 SQLite 副本上验证。
5.5 Pytest 结果
TEST.md 记录了两次运行输出:第一次收集 82 个用例(78 passed, 4 skipped),第二次为 subset 运行(66 passed, 3 skipped)。完整输出可见 TEST.md,其中 4 个 skipped 均为 test_full_e2e.py 中的 opt-in 写测试(test_opt_in_write_import_commands、test_opt_in_import_json_with_inline_attachment、test_opt_in_note_add_command),以及真实库中不存在保存搜索时跳过的 test_search_detail_commands。这些断言同时也印证了 _helpers.py 中 fake 服务对 /connector/ping、/connector/import、/connector/saveItems、/connector/updateSession、/connector/saveAttachment、/v1/responses、/api/users/0/...、/api/groups/<id>/... 等端点的完整模拟能力。
六、测试背后的关键设计约定
TEST.md 末尾的 Notes 是理解整个 harness 设计哲学的钥匙,可结合源码进一步展开:
- 只读 immutable SQLite 连接:SQLite 检查使用只读不可变连接,因此 Zotero 保持打开时本地读取也能正常工作——这是"Zotero 不必运行"类命令(
collection list/find/tree/get/items、item list/get/children/notes/attachments/file、tag list/items、style list等)的底层保障; - bare key 解析是 library-aware 的:唯一 key 自动解析;跨库重复 key 抛
AmbiguousReferenceError,要求先session use-library <id>消歧; - Local API 读/导出路由双形态稳定验证:同一套代码同时验证
/api/users/0/...与/api/groups/<libraryID>/...两条路由,保证用户库与群组库行为一致; - 实验性写入的三重保护:Zotero 必须关闭 + 必须显式
--experimental+ 每次写入前自动生成带时间戳的zotero.sqlite备份,且单事务内失败即回滚; item context是推荐的模型无关 AI 接口:它聚合条目字段、附件路径、可选笔记、可选 BibTeX/CSL JSON、可选 DOI/URL 链接,最终输出可直接投喂任意 LLM 的prompt_context文本块;item analyze则在此之上加一层 OpenAI Responses API 调用,要求OPENAI_API_KEY与显式--model;item analyze只做 mocked 验证:由 fake OpenAI 兼容子进程测试覆盖,不发起真实外部 API 调用。
七、如何复现这套测试
在仓库内复现的分步指南(对应 README.md 的 Testing 章节):
# 1. 安装 harness
py -m pip install -e .
# 2. 单元级测试(无需真实 Zotero)
py -m pytest cli_anything/zotero/tests/test_core.py -v
py -m pytest cli_anything/zotero/tests/test_cli_entrypoint.py -v
py -m pytest cli_anything/zotero/tests/test_agent_harness.py -v
# 3. 真实 Zotero E2E(本机需安装 Zotero 且有数据目录)
py -m pytest cli_anything/zotero/tests/test_full_e2e.py -v -s
# 4. 全量
py -m pytest cli_anything/zotero/tests/ -v --tb=no
# 5. 强制已安装命令模式
set CLI_ANYTHING_FORCE_INSTALLED=1
py -m pytest cli_anything/zotero/tests/test_cli_entrypoint.py -v
# 6. opt-in 真实写测试(默认关闭)
set CLI_ANYTHING_ZOTERO_ENABLE_WRITE_E2E=1
set CLI_ANYTHING_ZOTERO_IMPORT_TARGET=<collection-key-or-id>
py -m pytest cli_anything/zotero/tests/test_full_e2e.py -v -s
适用前提:Python 3.10+(README 要求);前两类测试完全不依赖 Zotero 安装;test_full_e2e.py 需要本机存在 Zotero profile 与数据目录,找不到时自动 skip;写类 E2E 必须显式 opt-in,且实验性 SQLite 写入永远只针对隔离副本。
八、总结
这套测试体系的价值在于它精确映射了 harness 的三种后端形态:SQLite 只读盘点(离线、Zotero 不必运行)、connector 写通道(官方写流程、需 Zotero 运行且 GUI 在正确库上)、Local API 检索/渲染(需启用 Local API)。测试分层让"纯逻辑"与"真实桌面集成"互不阻塞:单测用 _helpers.py 现场构造的模拟 Zotero 环境保证可重复性,子进程测试用 fake HTTP 服务验证完整调用链,E2E 测试则在真实 Zotero 上做最终确认,并将所有写操作默认隔离在 opt-in 与隔离 SQLite 副本的严格边界内。这种"能离线测的绝不依赖 GUI、能 mock 的绝不碰真实服务、会写数据的必须显式授权"的策略,是 agent-native 工具验证的可靠范本。