☰
Obsidian Copilot 测试实战指南:从 Jest 单元测试到 Obsidian CLI 端到端验证
2026/9/27 8:09:46 网站建设 项目流程
  • AI 应用
  • 大模型
  • AI Agent
  • 交互助手
  • RAG

【免费下载链接】obsidian-copilot

Run agents in Obsidian - OpenCode, Codex, Claude Code etc.

项目地址:https://gitcode.com/gh_mirrors/ob/obsidian-copilot
点击查看免费下载

Obsidian Copilot 插件的完整测试体系实战指南。本文围绕仓库中 designdocs/agents/TESTING_GUIDE.md 展开,覆盖单元测试设计工作流、基于 Obsidian 桌面版 CLI 的端到端测试、设置数据重置、组件画廊(component gallery)验证与性能快照采集。读完本文,你将掌握一条从"写行为描述 → 跑红绿灯测试 → 部署进真实 vault → 用 CLI 驱动并采集证据"的完整可执行路径,能够独立为涉及 React 树、真实 DOM 与设置持久化的改动建立可复现的验证闭环。

测试金字塔:什么时候该用哪一层

仓库明确采用两层测试策略,取舍标准只有一个——单元测试能否回答这个问题:

层级命令 / 手段适用场景
单元测试npm run test纯逻辑、运行快、Mock 掉 Obsidian API。任何不直接触碰 Obsidian 自身的代码改动,默认都写单元测试
端到端(E2E)obsidianCLI + 真实 vault需要真实 React 树、真实 Obsidian DOM 或真实设置持久化的场景——UI 回归、设置往返(round-trip)、插件生命周期、性能检查

E2E 是最慢、最脆弱的一层,只有当单元测试回答不了问题时才动用它。这一判断背后有明确的结构支撑:仓库在 designdocs/agents/STYLE_GUIDE.md 中要求"可测试代码"——依赖注入、纯逻辑下沉到叶子模块、getSettings()等单例只允许出现在顶层编排处,并给出了一个金标准(litmus test):"能否用普通参数直接调用它完成测试?"如果答案是否,说明这个依赖应该作为参数传入。按照这一结构写出来的代码,天然大部分都能用纯单元测试覆盖,E2E 只需补齐单元测试够不到的部分。

单元测试:可执行规格说明

基础设施与运行方式

  • Jest + TypeScript(ts-jest),npm run test运行整个单测套件;单独跑一个用例用npm test -- -t "test name"。
  • 测试文件与被测实现放在同一目录,命名*.test.ts/*.test.tsx。
  • React 组件测试使用@testing-library/react。

测试环境的搭建细节可从 jest.config.js 与 jest.setup.js 中确认:

  • testEnvironment为jsdom,roots覆盖src、dev、scripts三个目录;
  • moduleNameMapper将^obsidian$指向mocks/obsidian.js,并把@agentclientprotocol/sdk、@anthropic-ai/claude-agent-sdk、react-resizable-panels等外部依赖替换为仓库内的桩实现;
  • jest.setup.js 为 jsdom 补齐了 Obsidian 运行环境:TextEncoder/TextDecoder、matchMedia、Node.doc/Node.win增强、createEl/createDiv/createSpan等 DOM 创建助手、HTMLElement.setCssProps、addClass、setText,以及activeDocument/activeWindow全局对象——这些 polyfill 意味着插件代码在 jsdom 下可以像在真实 Obsidian 里一样构建界面,这正是"Mock 掉 Obsidian API"的具体落地方式。

测试设计工作流

单元测试是可执行的规格说明。一个开发者或 Agent 应该能仅凭describe/it的骨架,就讲清楚模块做了什么、公开操作如何影响可观察状态或输出、以及它保持了哪些约束。套件全绿本身并不等于行为被正确覆盖。

新增或修改行为时,按以下五步执行:

  1. 先写行为大纲。从需求契约推导用例:对每个受影响的可调用对象,先建立其正常使用路径,再补充边界、失败与回归用例。如果现有套件只覆盖了边界用例,先补上缺失的正常行为。不要把实现分支照搬进测试,也不要为不支持的假想状态发明用例。
  2. 命名"条件与结果"。把每个describe加it读成一句完整的话,说清楚"什么变了 / 返回了什么 / 什么时候发生"。避免"works"、"handles updates"、"publishes once"这类不解释行为的命名。必须保留的 issue URL 保留,但要确保不看 issue 也能读懂描述。
  3. 让测试体证明命名的主张。使用具体输入和有意义的 fixture 名称,明确划分 arrange / act / assert 三部分。一个用例只承载一个行为场景,可以用多个断言来确立该场景。断言可观察的结果——调用计数只在"通知/交互属于契约的一部分"时才有用,不能代替检查承诺的状态或输出。助手函数用来消除搭建噪音,但要让决定性输入和结果保持可见。
  4. 红绿重构(Red, Green, Refactor)。在实现行为之前先跑新测试,确认它是因为承诺的行为缺失或错误而失败,而不是因为 setup、import 或 mock 坏了。做最小改动使其通过,然后在不破坏绿色的前提下改进代码。为已工作行为补覆盖时,先临时破坏该行为,验证测试确实能发现,再恢复。不要声称经历过没有实际观察到的红阶段。
  5. 复查大纲与断言。只读测试名(不看测试体):新人能否借此讲清受影响模块的契约?再逐个检查测试体:如果命名的行为坏了,它真的会失败吗?修复覆盖或断言缺口;只给弱测试改名是不够的。

文档给出的示范是select()套件应该按"去重之前的选择行为"组织:

  • it("makes the chosen chat the context source for Relevant Notes", ...)
  • it("switches the context source to another chat before notifying subscribers", ...)
  • it("does not notify subscribers again when the same chat context object is reselected", ...)

这套工作流践行的是"测试先以规格说明的方式表达意图,再让它们通过"的原则(源自 Robert C. Martin 的 Test First 主张)。仓库中大量*.test.ts文件正是按这一模式组织的,例如 src/agentMode/session 下的测试套件,名称即可读作行为契约。

E2E:通过 Obsidian CLI 驱动真实 vault

E2E 是给编码 Agent 使用的"田野指南":用 Obsidian 桌面版 CLI 驱动 Copilot 插件。文档中的命令均基于 Obsidian1.12.7在真实 vault 中验证。CLI 位于/Applications/Obsidian.app/Contents/MacOS/obsidian——务必使用完整路径,obsidianshim 不一定在PATH上。

部署新构建到测试 vault

npm run test:vault

仅限 macOS。该命令安装依赖、构建,并把当前工作树(worktree)的main.js/manifest.json/styles.css复制进$COPILOT_TEST_VAULT_PATH/.obsidian/plugins/copilot/,然后通过 Obsidian CLI 重载插件。前提是$COPILOT_TEST_VAULT_PATH(用户级环境变量)指向一个至少被 Obsidian 打开过一次的 vault。

这是"让我的改动跑起来"的规范步骤——不要手搓npm run build && cp main.js …。实现细节见 scripts/test-vault.sh:

  • 会拒绝在工作树位于目标 vault 内部时执行(避免把源码变成 vault 内容、避免把插件部署到自身之上);
  • 复制的 manifest 会带上开发版版本后缀,并以可见标签携带 commit、clean/dirty 状态和 bundle 哈希([dev build: <commit>-<state>-<hash> | branch: <branch> | built: <ts>]);
  • 重载采用 disable → enable 循环,而不是plugin:reload——在该环境下plugin:reload返回成功但不会重新执行插件的onload,新部署的main.js根本不会运行;
  • 关键坑:Obsidian CLI 的目标 vault 由当前工作目录决定(它会解析 $PWD 所在的 vault,vault=参数无法覆盖这一点),因此脚本必须cd "$VAULT_PATH"后再调用 CLI,否则重载会悄悄打到调用者 cwd 所在的 vault 上;
  • 若多个 Conductor 工作树共用同一个测试 vault,最后一次执行test:vault的那个生效——用下一节的 preflight 验证,不要假设;
  • 之后工作树的再次构建不会改动已部署的插件,需要重新执行test:vault才会部署并重载新构建。

重置测试 vault 的设置

npm run test:reset-data # 使用内置的 clean-onboarding fixture npm run test:reset-data -- <path> # 使用你指定的任意 data.json

它是test:vault的搭档:用 fixture 覆盖$COPILOT_TEST_VAULT_PATH/.obsidian/plugins/copilot/data.json,然后重载插件(与test:vault相同的 Obsidian CLI 重载)使重置立即生效——重载很重要,因为运行中的插件在内存中持有设置,否则会在下次保存时把文件覆盖回去。实现见 scripts/test-reset-data.sh:在触碰目标之前会先用 Node 校验源文件是合法 JSON,防止损坏的 fixture 污染 vault。

无参数时使用 scripts/test-fixtures/data.clean-onboarding.json:不配置任何编码 Agent、也没有 BYOK 模型(providers/configuredModels/backends均为空),用于从零开始测试 Agent 引导流程。该 fixture 刻意省略_keychainOnly,因此插件以磁盘模式加载,忽略 OS 钥匙串中遗留的任何 API key——一个确定性的干净状态。

data.legacy-byok.json——一次性 BYOK 迁移

npm run test:reset-data -- scripts/test-fixtures/data.legacy-byok.json加载一个版本前的旧安装(无settingsVersion;旧的activeModels+ 顶层 provider key;新的数据切片为空,仅预置了一个用于验证去重的 BYOK Anthropic provider)。下次插件加载时,runSettingsMigrations会把旧版 BYOK providers/models 转换为新的providers/configuredModels/backends形状,使其能在 OpenCode 中工作,并盖上settingsVersion印章。它以磁盘模式运行,使用明显的假sk-test-…key。

迁移后的验证命令:

$OBS vault=$VAULT eval code='app.plugins.plugins.copilot.loadData().then(d=>JSON.stringify({ settingsVersion: d.settingsVersion, providers: Object.values(d.providers).map(p=>({type:p.providerType, origin:p.origin.kind, catalog:p.origin.catalogProviderId, hasKey: p.apiKeyKeychainId!=null})), configuredModels: d.configuredModels.map(m=>m.info.id), chat: d.backends.chat?.enabledModels?.length, opencode: d.backends.opencode?.enabledModels?.length, legacyUntouched: { anthropicApiKey: d.anthropicApiKey, activeModels: d.activeModels.length } }))'

预期结果:settingsVersion已盖章;可路由的 provider 带有catalogProviderId和 key;backends.opencode.enabledModels非空且排除了 embedding(BAAI/bge-m3)与 Bedrock 行;恰好一个 Anthropic BYOK provider(预置的那个——其描述符被去重,而非重建);旧版anthropicApiKey与activeModels保持原样。第二次重载不得改变settingsVersion或产生重复(版本门控生效)。

这一行为在源码中有完整印证:src/settings/migrations/index.ts 的runSettingsMigrations是逐版本门控的顺序执行器——只有fromVersion < 4的 vault 才跑 BYOK 迁移,最终无条件把settingsVersion升到CURRENT_SETTINGS_VERSION,即使某个 provider 迁移失败也不会卡住门控反复重跑。

0. 金律:先选对窗口

Obsidian 是单个 Electron 应用,每个打开的 vault 有一个 renderer。CLI 一次只与一个renderer 通信,默认目标是"最近被触碰的 vault"(焦点 + 上次 CLI 目标——跨调用不稳定)。开发机上同时开着多个 vault 时,几乎必然出现"本地明明好好的"这种假象。

每条命令都必须带vault=<name>。该 flag 是全局的(对所有子命令生效),把调用绑定到特定 renderer:

OBS=/Applications/Obsidian.app/Contents/MacOS/obsidian VAULT="$(basename "$COPILOT_TEST_VAULT_PATH")" $OBS vault=$VAULT vault # 回显目标 vault 的名称/路径
任何测试运行前的验证门

运行 preflight,有任何异常立即中止:

$OBS vault=$VAULT eval code='JSON.stringify({ vault: app.vault.getName(), path: app.vault.adapter.basePath, copilotLoaded: !!app.plugins.plugins.copilot, copilotVersion: app.plugins.manifests.copilot?.version, buildTag: app.plugins.manifests.copilot?.description?.match(/dev build: ([^ |]+)/)?.[1], buildBranch: app.plugins.manifests.copilot?.description?.match(/branch: ([^ |]+)/)?.[1] })'

检查项:

  • vault与预期目标一致;
  • copilotLoaded为true(vault=<name>会静默地打开一个已知但关闭的 vault 作为新 renderer,插件可能还没就绪;若为false,通过eval调用app.plugins.loadPlugin("copilot")再重新探测);
  • buildTag以当前工作树的短 commit 开头、报告预期的 clean/dirty 状态,且在下次部署前保持不变;
  • buildBranch与你实际构建的分支一致——多个 Conductor 工作区共享一个测试 vault 时,最后运行npm run test:vault的胜出——验证,不要假设。
需要识别的失败模式
症状原因修复
Vault not found.vault=<name>拼写错误(区分大小写,匹配vaults verbose显示的 vault 名)运行$OBS vaults verbose列出已知 vault
命令以 0 退出但 stdout 为空,eval无返回vault 已注册但 renderer 尚未打开——Obsidian 正在启动它sleep 2 && retry,然后验证
默认 vault 在命令之间反复变化你漏了vault=<name>——焦点移动了始终传 flag,绝不依赖默认值
截图显示的是另一个 vault同上——捕获的是焦点 renderer 而非目标 renderer显式带vault=<name>重拍

1. 每次会话只设置一次

$OBS dev:debug on # 附加 Chrome DevTools 协议(dev:console、dev:errors、dev:cdp 的前置条件) $OBS vault=$VAULT eval code='app.plugins.loadPlugin("copilot")' # 若未自动加载

dev:debug on在 Obsidian 进程生命周期内是粘性的,重复运行是 no-op;但 Obsidian 重启后必须重新执行,否则dev:console、dev:errors、dev:cdp都不会返回有用结果。

2. 驱动插件

运行命令:

$OBS vault=$VAULT command id=copilot:agent-chat-open-window

完整命令 ID 清单:

$OBS vault=$VAULT eval code='JSON.stringify(Object.keys(app.commands.commands).filter(c=>c.startsWith("copilot")))'

注意:顶层obsidian commands只列出 Obsidian 核心命令,插件命令只能通过app.commands.commands看到。

编程方式读写设置:设置既存在于磁盘上的data.json,也存在于运行时的 Jotai atom store。测试最快速、最确定性的路径是绕过 UI:

# 读 $OBS vault=$VAULT eval code='app.plugins.plugins.copilot.loadData().then(d=>JSON.stringify({temperature:d.temperature,defaultChainType:d.defaultChainType}))' # 写并持久化 $OBS vault=$VAULT eval code='(async()=>{ const p=app.plugins.plugins.copilot; const d=await p.loadData(); d.temperature=0.42; await p.saveData(d); return (await p.loadData()).temperature; })()'

Gotcha:saveData()会写入磁盘,但不会推入已打开设置弹窗的 React 状态。如果需要 UI 反映改动,请关闭并重新打开设置(command id=app:open-settings),或者干脆通过 UI 本身驱动(见第 4 节)。

3. 捕获证据

截图——dev:screenshot:

$OBS vault=$VAULT dev:screenshot path=/tmp/before.png sleep 1 # 文件是异步写入的;CLI 返回时可能还没落盘

然后用 Read 工具读取 PNG 做视觉检查。路径可以是绝对路径(/tmp/foo.png)或 vault 相对路径。永远搭配vault=<name>flag——否则可能捕获到处于焦点的那个 Obsidian 窗口。一个有用的 sanity check:裁切/检查底部状态栏,它会显示 vault 名。

DOM 查询——dev:dom(只读):默认返回outerHTML,也可返回文本、属性或计算样式:

$OBS vault=$VAULT dev:dom selector='.vertical-tab-nav-item.is-active .vertical-tab-nav-item-title' text $OBS vault=$VAULT dev:dom selector='.modal.mod-settings input[type="text"]' all $OBS vault=$VAULT dev:dom selector='.copilot-chat-input' attr=placeholder

复杂检查(循环、结构化数据)优先用eval+document.querySelectorAll——dev:dom输出很快会变得冗长,且 CLI 是同步流式返回的。

控制台与错误:

$OBS vault=$VAULT dev:console limit=30 $OBS vault=$VAULT dev:console level=error limit=20 $OBS vault=$VAULT dev:errors clear # 场景开始前重置 $OBS vault=$VAULT dev:errors # 场景结束后采集

dev:errors只捕获未捕获的错误。测试模式:clear → 跑场景 → 等待 → 重新读取。计数大于零意味着回归。

性能快照:

$OBS vault=$VAULT dev:cdp method=Performance.enable $OBS vault=$VAULT dev:cdp method=Performance.getMetrics # JSHeap*、Nodes、JSEventListeners、ScriptDuration 等 $OBS vault=$VAULT dev:cdp method=Memory.getDOMCounters # { documents, jsEventListeners, nodes } $OBS vault=$VAULT eval code='JSON.stringify(performance.memory)'

动作计时(两次 RAF 强制 layout + paint 提交):

$OBS vault=$VAULT eval code='(async()=>{ const t0=performance.now(); document.querySelector("[data-setting-id=\"copilot\"]").click(); await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))); return performance.now()-t0; })()'

泄漏检测的 diff 模式:快照Memory.getDOMCounters→ 重复执行该功能 N 次 → 再快照。若nodes或jsEventListeners持续攀升超过 N 次迭代,说明该功能在泄漏。

4. 与 UI 交互(点击、输入)

CLI 没有一等公民的"点击选择器"或"向输入框打字"命令,两条真实可行的路线:

4a.eval——合成 JS 事件(快、容易、略假)

对大多数插件 UI 有效:

# 点按钮 $OBS vault=$VAULT eval code='document.querySelector(".send-button")?.click()' # 给 React 受控输入赋值 $OBS vault=$VAULT eval code=' const el = document.querySelector(".copilot-chat-input"); const setter = Object.getOwnPropertyDescriptor(el.constructor.prototype,"value").set; setter.call(el, "hello"); el.dispatchEvent(new Event("input",{bubbles:true})); '

注意事项:合成事件的isTrusted=false,任何以此为门槛的逻辑都不会触发;Lexical / contentEditable 表面通常需要beforeinput而不是input。

4b.dev:cdp——真实的浏览器级输入(慢、忠实)

当合成事件被拒绝、需要isTrusted=true,或者要验证 UI 本身行为正确(focus、校验器、onChange、modal)时使用:

# 先取目标坐标 $OBS vault=$VAULT eval code=' const el = document.querySelector("[data-setting-id=\"copilot\"]"); const r = el.getBoundingClientRect(); JSON.stringify({x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)}) ' # 真实点击 $OBS vault=$VAULT dev:cdp method=Input.dispatchMouseEvent params='{"type":"mousePressed","x":174,"y":764,"button":"left","clickCount":1}' $OBS vault=$VAULT dev:cdp method=Input.dispatchMouseEvent params='{"type":"mouseReleased","x":174,"y":764,"button":"left","clickCount":1}' # 聚焦输入框,再通过浏览器输入层打字 $OBS vault=$VAULT eval code='document.querySelector(".copilot-chat-input").focus()' $OBS vault=$VAULT dev:cdp method=Input.insertText params='{"text":"hello from cdp"}' $OBS vault=$VAULT dev:cdp method=Input.dispatchKeyEvent params='{"type":"keyDown","key":"Enter","code":"Enter","windowsVirtualKeyCode":13}'

已验证端到端:CDP 的Input.insertText写入设置字段会沿 React onChange 路径传播并持久化到data.json。设置弹窗的 React 状态保持同步(与saveData路径不同)。

4c. Popout 窗口——第二个定位陷阱

即使vault=<name>把你带到正确的 renderer,Obsidian 还支持popout 窗口(把视图拆到独立 Electron 窗口)。CLI 的附加点只能是主 vault renderer:

  • eval的document/window永远指向主窗口;
  • dev:dom和dev:cdp Input.*操作的是主窗口的 document 和输入层;
  • dev:screenshot只捕获主 vault 窗口——popout 不在画面里。

要从 CLI 触达 popout,遍历app.workspace.floatingSplit:

$OBS vault=$VAULT eval code='JSON.stringify({ popouts: app.workspace.floatingSplit?.children?.length || 0, popoutTypes: (app.workspace.floatingSplit?.children || []).map(c=>c.children?.[0]?.children?.[0]?.view?.getViewType?.()) })'

查询 popout 的 DOM 使用 Obsidian 附加到每个节点上的.win/.doc属性:

$OBS vault=$VAULT eval code=' const popout = app.workspace.floatingSplit.children[0]; const doc = popout.win.document; doc.querySelector(".copilot-chat-input")?.value '

对 popout 做 UI 输入时,优先用针对popout.win.document.querySelector(...)的eval合成事件。CDP 层Input.*事件只去主窗口,到不了 popout——当前 CLI 表面无法绕开。如果你的测试场景依赖 popout 交互,便宜的修复是不把视图弹出去:全部在主侧栏 leaf 中驱动。

相关运行时陷阱(插件代码而非测试代码):标准的instanceof检查跨 realm 会失败——popout 窗口有自己独立的HTMLElement、MouseEvent等构造函数。在eval内部需要跨 realm 类型检查时,用element.instanceOf(HTMLElement)/event.instanceOf(MouseEvent)。

5. Vault 数据准备

要获得确定性测试 fixture,优先通过 CLI 创建笔记,而不是手改 vault:

$OBS vault=$VAULT create name="test-note" content="# Hello\n\nbody" $OBS vault=$VAULT append file="test-note" content="\nmore text" $OBS vault=$VAULT delete file="test-note" permanent $OBS vault=$VAULT search query="copilot" format=json limit=10

更激进的 reset 可以直接操作 vault 路径下的磁盘文件(路径来自app.vault.adapter.basePath),然后调用$OBS vault=$VAULT reload让 Obsidian 重新扫描。

组件画廊工作流

当功能新增或修改用户可见的 React 组件或重要的视觉状态时,使用组件画廊。非视觉的后端、数据或工具类改动不要求 story。画廊验证是对可调用级单元覆盖的补充,不能替代它。代表性的设置、打开菜单、权限 diff 与过渡状态检查清单及剩余缺口,见 designdocs/GALLERY_REVIEW_COVERAGE.md。

编写 story

  1. 在组件旁新增或更新*.stories.tsx文件。生成的画廊索引自动发现src/**/*.stories.tsx;不要编辑dev/gallery/stories.generated.ts。
  2. 从@/lib/story导入Meta和StoryObj,用satisfies Meta<Props>声明组件元数据,每个具名 story 都标注StoryObj<Props>。该模块的契约见 src/lib/story.ts:Host = "leaf" | "modal" | "popover" | "settings-tab",Layout = "padded" | "centered" | "fullscreen",另支持coverage与modalClass参数。
  3. 覆盖用户真正能看到的承重状态:default、empty、loading、success、error、disabled、容易溢出的内容,以及该功能引入的其他状态。使用写实的文案与 fixture props,而不是生产 store 或运行时单例。
  4. 普通 prop 状态优先用args,组合场景用render。支持 hook 驱动的 render 函数。保持 fixture 确定性、action 惰性——除非交互本身就是被测行为。
  5. 用parameters.gallery.host(leaf、modal、popover或settings-tab)与layout(padded、centered、fullscreen)匹配真实组件边界。只有有意的"纯展示组件豁免"才用coverage: false。story 不能钉死画布宽度——宽度是画廊宽度工具栏持有的视图状态,所以要用该工具栏在其他宽度检查组件,或用audit()一次扫完全部宽度。永远不要添加一个与兄弟 story 唯一区别是"你希望它渲染的宽度"的 story;它会渲染得一模一样。
  6. 如果组件离开插件状态无法渲染,抽出或暴露一个接受所需数据作为 props 的展示边界;不要为了触达设置、store 或运行时单例而扩大画廊的 import 围栏。

一个最小的相邻 story 长这样:

import type { Meta, StoryObj } from "@/lib/story"; import { StatusCard, type StatusCardProps } from "./StatusCard"; const meta = { title: "Feature/Status Card", component: StatusCard, parameters: { gallery: { host: "leaf", layout: "padded" } }, } satisfies Meta<StatusCardProps>; export default meta; export const Error: StoryObj<StatusCardProps> = { args: { message: "The operation could not be completed.", tone: "error" }, };

进入真实验证前,先运行改动组件与 story 契约的聚焦单元测试,然后构建开发插件:

npm run gallery:build

通过npm run gallery:vault部署到COPILOT_TEST_VAULT_PATH配置的非生产测试 vault。任何 CLI 或 UI 变更之前,先验证解析出的 vault 名称和路径与该配置一致;永远不要依赖隐式焦点 renderer。

画廊与插件共享样式表

画廊的样式表通过把 src/styles/tailwind.css 拼接到自己的源码构建而来,因此携带几乎完整的生产样式表副本——而 Obsidian 会把每个已启用插件的styles.css全文档注入。两份副本以相同特异性进入同一级联,所以一份基于旧src/styles/tailwind.css构建的画廊副本会压过已部署的生产规则,让插件自己的视图渲染出改动前的行为。

npm run test:vault保持二者同步:只要 vault 安装了画廊插件,它就会从当前部署的工作树重建画廊并重新链接、重载。gallery:vault把整个源码目录符号链接进去,所以这也修复了因工作树被删除而悬空的链接;如果 vault 没有画廊插件,这步会被跳过。

如果某个 CSS 改动看起来没生效,先在 inspector 里检查同一选择器是否出现两次,再怀疑改动本身。

故事树保持顶层分类展开。选中分类标签显示其 contact sheet;对嵌套分类,标签同时折叠/展开子树——只想切换选中的 contact sheet 时用旁边的 chevron。切换不同 story 或 contact sheet 不会关闭你已经展开的分支。

Agent 验证循环

当开发专用组件画廊插件加载后,其类型化的window.__gallery句柄可以不依赖截图地选择并审计 story。句柄契约见 dev/gallery/main.ts:list(): string[]、show(id, options): Promise<void>、audit(options): Promise<AuditReport[]>。短异步调用可以同时设置awaitPromise与returnByValue。完整审计时,把最终结果存进页面并轮询——即使设置了awaitPromise,Obsidian CLI 也可能在长 CDP promise 落定前就返回。

# 发现精确的 story id $OBS vault=$VAULT dev:cdp method=Runtime.evaluate params='{"expression":"window.__gallery.list()","returnByValue":true}' # 渲染一个 story,等待其副作用与布局落定 $OBS vault=$VAULT dev:cdp method=Runtime.evaluate params='{"expression":"window.__gallery.show(\"UI/Button/Variants\",{width:300}).then(()=>true)","awaitPromise":true,"returnByValue":true}' # 用稳定身份 + 请求宽度定位已挂载的用例 $OBS vault=$VAULT dev:dom selector='[data-story="UI/Button/Variants"][data-story-width="300"]' all # 启动精确宽度扫描,不依赖 CLI 连接去 await 它 $OBS vault=$VAULT dev:cdp method=Runtime.evaluate params='{"expression":"window.__galleryRun={status:\"pending\"};window.__gallery.audit({widths:[300,340,400,600]}).then(value=>{window.__galleryRun={status:\"fulfilled\",value}},reason=>{window.__galleryRun={status:\"rejected\",reason:String(reason?.stack??reason)}});true","returnByValue":true}' # 反复读取直到 fulfilled 或 rejected。fulfilled 值每个宽度一个 AuditReport $OBS vault=$VAULT dev:cdp method=Runtime.evaluate params='{"expression":"window.__galleryRun","returnByValue":true}'

audit()返回形如[{theme,width,findings:[{story,check,detail}]}]的数组(接口定义见 dev/gallery/audit.ts,审计检查类型包括contrast、off-token-color、overflow、render-failure、unsupported-color、zero-size)。请求的宽度可以是任意正有限像素值;上面四个值与画廊可见按钮对应。通过该句柄,theme 是刻意只读的:切换 Obsidian 外观设置,再跑一次相同的宽度数组扫描。完整的自动化通过检查最后还要单独核对未捕获错误:

$OBS vault=$VAULT dev:errors clear # 运行上面的 store-and-poll 扫描并等待 fulfilled/rejected 状态 $OBS vault=$VAULT dev:errors

典型 E2E 流程:最小冒烟测试脚手架

OBS=/Applications/Obsidian.app/Contents/MacOS/obsidian VAULT="$(basename "$COPILOT_TEST_VAULT_PATH")" # 1. Preflight——验证目标 + 构建 $OBS vault=$VAULT eval code='JSON.stringify({ v: app.vault.getName(), loaded: !!app.plugins.plugins.copilot, ver: app.plugins.manifests.copilot?.version })' || exit 1 # 2. 附加调试器,清空错误缓冲区 $OBS vault=$VAULT dev:debug on $OBS vault=$VAULT dev:errors clear # 3. 基线截图 + 指标 $OBS vault=$VAULT dev:screenshot path=/tmp/test-before.png $OBS vault=$VAULT dev:cdp method=Memory.getDOMCounters > /tmp/dom-before.json # 4. 跑场景 $OBS vault=$VAULT command id=copilot:agent-chat-open-window sleep 1 $OBS vault=$VAULT eval code='document.querySelector(".copilot-chat-input")?.focus()' $OBS vault=$VAULT dev:cdp method=Input.insertText params='{"text":"test prompt"}' # 5. 采集证据 sleep 2 $OBS vault=$VAULT dev:screenshot path=/tmp/test-after.png $OBS vault=$VAULT dev:cdp method=Memory.getDOMCounters > /tmp/dom-after.json $OBS vault=$VAULT dev:console level=error limit=20 > /tmp/console-errors.txt $OBS vault=$VAULT dev:errors > /tmp/uncaught.txt # 6. 断言:错误为空、截图看起来正确、节点数没有爆掉

快速参考

需求命令
列出打开的 vaultvaults verbose
指定目标 vault每条命令加前缀vault=<name>
验证目标 vault 正确vault=<name> eval code='app.vault.getName()'
视觉确认vault=<name> dev:screenshot path=/tmp/x.png(读取前 sleep ≥1s)
未加载时加载插件vault=<name> eval code='app.plugins.loadPlugin("copilot")'
列出插件命令vault=<name> eval code='JSON.stringify(Object.keys(app.commands.commands).filter(c=>c.startsWith("copilot")))'
运行插件命令vault=<name> command id=copilot:<id>
读设置(磁盘)vault=<name> eval code='app.plugins.plugins.copilot.loadData().then(JSON.stringify)'
改设置(磁盘)vault=<name> eval code='(async()=>{const p=app.plugins.plugins.copilot;const d=await p.loadData();d.X=Y;await p.saveData(d);})()'
用 JS 点击vault=<name> eval code='document.querySelector("...").click()'
用 CDP 点击dev:cdp method=Input.dispatchMouseEvent params='{"type":"mousePressed",...}'
用 CDP 输入dev:cdp method=Input.insertText params='{"text":"..."}'
未捕获错误dev:errors(clear + run + read)
控制台消息dev:console level=error limit=20
DOM 规模dev:cdp method=Memory.getDOMCounters
堆内存eval code='JSON.stringify(performance.memory)'
完整性能指标dev:cdp method=Performance.getMetrics(先Performance.enable)

落地建议

把这套流程固化成每次改动的默认动作:先在 designdocs/agents/STYLE_GUIDE.md 的可测试结构指导下写可调用级单元测试(覆盖正常路径 + 边界 + 回归,命名读作契约),纯逻辑问题就地解决;只有需要真实 React 树、真实 DOM 或设置持久化时,才走npm run test:vault部署 + 带vault=<name>的 CLI 驱动 + 截图/控制台/DOM 计数器证据采集的 E2E 通道;涉及用户可见组件时,再叠加画廊 story 与window.__gallery.audit()宽度扫描。三条通道互补,共同构成对 designdocs/agents/TESTING_GUIDE.md 所述"单元测试优先、E2E 兜底、画廊补视觉"分层策略的完整执行。

  • AI 应用
  • 大模型
  • AI Agent
  • 交互助手
  • RAG

【免费下载链接】obsidian-copilot

Run agents in Obsidian - OpenCode, Codex, Claude Code etc.

项目地址:https://gitcode.com/gh_mirrors/ob/obsidian-copilot
点击查看免费下载
上一篇:5 分钟上手 Starship 配置:从安装到换主题的完整指南
下一篇:discord.py 终极入门:10分钟打造你的第一个 Discord Bot(Python新手友好)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询