Zotero CLI Harness 测试体系深度解析:从单元测试到真实 Zotero 的端到端验证

原创2026-09-09 19:05:591,602 阅读
文章标签:人工智能AI AgentAI 技能/插件CLI工具调用

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 工具验证的可靠范本。

登录后查看全文
CLI-Anything