Files
Octop/tests/unit/connectors/test_obsidian_cli.py
Frank ZhangandCursor 97d6e7ff45 feat(connectors): add Obsidian CLI as a built-in connector (#1856)
* feat(connectors): add Obsidian CLI as a built-in connector

Let agents search and edit one vault through the official desktop CLI, and show the official install guide in the connector drawer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(connectors): register Obsidian CLI per OS and satisfy ruff format

Linux must copy the bundled binary into ~/.local/bin, macOS keeps a symlink, and Windows leaves Obsidian.com beside the app so PATH registration still works.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(tests): expect relative plan paths on Windows drive workspaces

write_file rejects drive-letter paths, so Windows CI must keep plans/foo.md relative instead of asserting a rewritten absolute path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(setup): keep the first cold-install timeout exactly on budget

Subtracting a freshly stored monotonic deadline can yield 900.0000000000001, which failed the portable pip upgrade test on Linux.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(connectors): use the official Obsidian logo

The connector card was showing a placeholder gem. Replace it with the mark from obsidian.md and match the accent to that purple.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 21:53:39 +08:00

98 lines
3.5 KiB
Python

"""Obsidian CLI gateway adapter: vault scoping and command allowlist."""
from __future__ import annotations
from typing import Any
import pytest
from octop.infra.connectors.gateway import cli_install
from octop.infra.connectors.gateway.adapters import obsidian_cli
def _patch_binary(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
obsidian_cli,
"locate_obsidian_binary",
lambda _explicit=None: "obsidian-bin",
)
def test_search_argv_is_scoped_to_instance_vault(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def _run(argv: list[str], **_kwargs: Any) -> str:
seen["argv"] = argv
return "[]"
_patch_binary(monkeypatch)
monkeypatch.setattr(obsidian_cli, "run_cli", _run)
out = obsidian_cli.call_tool(
{"vault": "My Vault"},
"search",
{"query": "meeting", "vault": "Other", "limit": 5},
)
assert out == "[]"
argv = seen["argv"]
assert argv[0] == "obsidian-bin"
assert argv[1] == "vault=My Vault"
assert argv[2] == "search"
assert "query=meeting" in argv
assert "format=json" in argv
assert "limit=5" in argv
assert not any(part.startswith("vault=Other") for part in argv)
def test_help_rejects_eval_and_delete(monkeypatch: pytest.MonkeyPatch) -> None:
def _run(*_args: object, **_kwargs: object) -> str:
raise AssertionError("denied commands must not reach the CLI")
_patch_binary(monkeypatch)
monkeypatch.setattr(obsidian_cli, "run_cli", _run)
with pytest.raises(ValueError, match="不允许执行"):
obsidian_cli.call_tool({"vault": "Notes"}, "help", {"command": "eval"})
with pytest.raises(ValueError, match="不允许执行"):
obsidian_cli.call_tool({"vault": "Notes"}, "help", {"command": "delete"})
with pytest.raises(ValueError, match="不允许执行"):
obsidian_cli.call_tool({"vault": "Notes"}, "help", {"command": "dev:screenshot"})
def test_search_rejects_newline_query(monkeypatch: pytest.MonkeyPatch) -> None:
def _run(*_args: object, **_kwargs: object) -> str:
raise AssertionError("should not run")
_patch_binary(monkeypatch)
monkeypatch.setattr(obsidian_cli, "run_cli", _run)
with pytest.raises(ValueError, match="换行"):
obsidian_cli.call_tool({"vault": "Notes"}, "search", {"query": "a\nb"})
def test_probe_missing_binary(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cli_install.shutil, "which", lambda _name: None)
monkeypatch.setattr(cli_install, "obsidian_bundle_candidates", lambda: [])
with pytest.raises(ValueError, match="未找到 Obsidian CLI"):
obsidian_cli.probe_credentials({"vault": "Notes"})
def test_probe_unknown_vault(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_binary(monkeypatch)
def _run(argv: list[str], **_kwargs: Any) -> str:
assert argv[:3] == ["obsidian-bin", "vault=Missing", "vault"]
raise ValueError("Unknown vault Missing")
monkeypatch.setattr(obsidian_cli, "run_cli", _run)
with pytest.raises(ValueError, match="找不到库"):
obsidian_cli.probe_credentials({"vault": "Missing"})
def test_probe_app_not_running(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_binary(monkeypatch)
def _run(*_args: object, **_kwargs: object) -> str:
raise ValueError("Obsidian is not running")
monkeypatch.setattr(obsidian_cli, "run_cli", _run)
with pytest.raises(ValueError, match="未在运行"):
obsidian_cli.probe_credentials({"vault": "Notes"})