- AI 应用
- 大模型
- AI Agent
- 交互助手
- RAG
【免费下载链接】obsidian-copilot
Run agents in Obsidian - OpenCode, Codex, Claude Code etc.
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的骨架,就讲清楚模块做了什么、公开操作如何影响可观察状态或输出、以及它保持了哪些约束。套件全绿本身并不等于行为被正确覆盖。
新增或修改行为时,按以下五步执行:
- 先写行为大纲。从需求契约推导用例:对每个受影响的可调用对象,先建立其正常使用路径,再补充边界、失败与回归用例。如果现有套件只覆盖了边界用例,先补上缺失的正常行为。不要把实现分支照搬进测试,也不要为不支持的假想状态发明用例。
- 命名"条件与结果"。把每个
describe加it读成一句完整的话,说清楚"什么变了 / 返回了什么 / 什么时候发生"。避免"works"、"handles updates"、"publishes once"这类不解释行为的命名。必须保留的 issue URL 保留,但要确保不看 issue 也能读懂描述。 - 让测试体证明命名的主张。使用具体输入和有意义的 fixture 名称,明确划分 arrange / act / assert 三部分。一个用例只承载一个行为场景,可以用多个断言来确立该场景。断言可观察的结果——调用计数只在"通知/交互属于契约的一部分"时才有用,不能代替检查承诺的状态或输出。助手函数用来消除搭建噪音,但要让决定性输入和结果保持可见。
- 红绿重构(Red, Green, Refactor)。在实现行为之前先跑新测试,确认它是因为承诺的行为缺失或错误而失败,而不是因为 setup、import 或 mock 坏了。做最小改动使其通过,然后在不破坏绿色的前提下改进代码。为已工作行为补覆盖时,先临时破坏该行为,验证测试确实能发现,再恢复。不要声称经历过没有实际观察到的红阶段。
- 复查大纲与断言。只读测试名(不看测试体):新人能否借此讲清受影响模块的契约?再逐个检查测试体:如果命名的行为坏了,它真的会失败吗?修复覆盖或断言缺口;只给弱测试改名是不够的。
文档给出的示范是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
- 在组件旁新增或更新
*.stories.tsx文件。生成的画廊索引自动发现src/**/*.stories.tsx;不要编辑dev/gallery/stories.generated.ts。 - 从
@/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参数。 - 覆盖用户真正能看到的承重状态:default、empty、loading、success、error、disabled、容易溢出的内容,以及该功能引入的其他状态。使用写实的文案与 fixture props,而不是生产 store 或运行时单例。
- 普通 prop 状态优先用
args,组合场景用render。支持 hook 驱动的 render 函数。保持 fixture 确定性、action 惰性——除非交互本身就是被测行为。 - 用
parameters.gallery.host(leaf、modal、popover或settings-tab)与layout(padded、centered、fullscreen)匹配真实组件边界。只有有意的"纯展示组件豁免"才用coverage: false。story 不能钉死画布宽度——宽度是画廊宽度工具栏持有的视图状态,所以要用该工具栏在其他宽度检查组件,或用audit()一次扫完全部宽度。永远不要添加一个与兄弟 story 唯一区别是"你希望它渲染的宽度"的 story;它会渲染得一模一样。 - 如果组件离开插件状态无法渲染,抽出或暴露一个接受所需数据作为 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. 断言:错误为空、截图看起来正确、节点数没有爆掉快速参考
| 需求 | 命令 |
|---|---|
| 列出打开的 vault | vaults 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.
相关推荐
cli-anything-obsidian 测试计划解读:Mock 单元测试与真实 Obsidian 端到端验证实战指南
cli anything obsidian 测试计划解读:Mock 单元测试与真实 Obsidian 端到端验证实战指南 导读 cli anything obs
人工智能AI AgentAI 技能工具调用CLICANN/asc-devkit BitwiseXor临时缓冲区因子大小
GetBitwiseXorTmpBufferFactorSize<a name="ZH CN_TOPIC_0000002441333404" </a 功能说明<
人工智能深度学习算子库CANNAscendBentoML 服务测试指南:从单元测试到端到端验证的完整实战
BentoML 服务测试指南:从单元测试到端到端验证的完整实战 本文以 BentoML 官方文档《Test API endpoints》为核心骨架,系统讲解如何
模型推理服务人工智能后端大模型MLOpsLLMOps
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考