feat: add mycursor backend; drop myagents xiaohe entrypoint
Cursor Agent CLI wrapper (mycursor) with resume/session listing. Product xiaohe CLI stays in xiaohe-api only — no dual entry.
This commit is contained in:
+197
-1
@@ -15,7 +15,7 @@ class TestMyagentsHelp:
|
||||
"""Tests for top-level myagents command."""
|
||||
|
||||
def test_help_shows_agent_subcommands(self) -> None:
|
||||
"""--help should list claude, kimi, codex and hermes subcommands."""
|
||||
"""--help should list claude, kimi, codex, hermes and cursor subcommands."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
@@ -23,6 +23,7 @@ class TestMyagentsHelp:
|
||||
assert "kimi" in result.output
|
||||
assert "codex" in result.output
|
||||
assert "hermes" in result.output
|
||||
assert "cursor" in result.output
|
||||
assert "update" in result.output
|
||||
assert "upgrade" in result.output
|
||||
|
||||
@@ -49,6 +50,7 @@ class TestMyagentsHelp:
|
||||
assert "kimi" in result.output
|
||||
assert "codex" in result.output
|
||||
assert "hermes" in result.output
|
||||
assert "cursor" in result.output
|
||||
|
||||
def test_help_lists_backends(self) -> None:
|
||||
"""--help should list backends subcommand."""
|
||||
@@ -598,3 +600,197 @@ class TestHermesListSessions:
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "sess-abc123" in result.output
|
||||
assert "hello hermes" in result.output
|
||||
|
||||
|
||||
class TestCursorSubcommand:
|
||||
"""Tests for ``myagents cursor``."""
|
||||
|
||||
def test_help_shows_options(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["cursor", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--cwd" in result.output
|
||||
assert "--list" in result.output
|
||||
|
||||
def test_runs_agent(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/agent"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["cursor"])
|
||||
assert result.exit_code == 0
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/agent", "--force"]
|
||||
|
||||
def test_falls_back_to_cursor_agent(self) -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
def _which(name: str) -> str | None:
|
||||
return "/usr/bin/cursor-agent" if name == "cursor-agent" else None
|
||||
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", side_effect=_which),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
patch.dict("os.environ", {}, clear=False) as env,
|
||||
):
|
||||
env.pop("CURSOR_BIN", None)
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["cursor"])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == [
|
||||
"/usr/bin/cursor-agent",
|
||||
"--force",
|
||||
]
|
||||
|
||||
def test_missing_binary_error(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value=None),
|
||||
patch.dict("os.environ", {}, clear=False) as env,
|
||||
):
|
||||
env.pop("CURSOR_BIN", None)
|
||||
result = runner.invoke(cli, ["cursor"])
|
||||
assert result.exit_code == 127
|
||||
assert "not found" in result.output.lower()
|
||||
|
||||
def test_cwd_option_passed(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
test_dir = tmp_path / "test_cwd"
|
||||
test_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/agent"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["cursor", "--cwd", str(test_dir)])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args.kwargs.get("cwd") == str(test_dir.resolve())
|
||||
|
||||
|
||||
class TestCursorPassthrough:
|
||||
"""Map myagents-style resume flags to Cursor Agent CLI flags."""
|
||||
|
||||
def _invoke(self, args: list[str], tmp_path: Path) -> list[str]:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/agent"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
cli, ["cursor", "--cwd", str(tmp_path), *args]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_run.assert_called_once()
|
||||
return mock_run.call_args[0][0]
|
||||
|
||||
def test_r_flag_maps_to_resume(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["-r", "abc123"], tmp_path)
|
||||
assert cmd == ["/usr/bin/agent", "--force", "--resume", "abc123"]
|
||||
|
||||
def test_bare_r_flag_maps_to_resume(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["-r"], tmp_path)
|
||||
assert cmd == ["/usr/bin/agent", "--force", "--resume"]
|
||||
|
||||
def test_resume_flag_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["--resume", "abc123"], tmp_path)
|
||||
assert cmd == ["/usr/bin/agent", "--force", "--resume", "abc123"]
|
||||
|
||||
def test_c_flag_maps_to_continue(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["-c"], tmp_path)
|
||||
assert cmd == ["/usr/bin/agent", "--force", "--continue"]
|
||||
|
||||
def test_continue_flag_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["--continue"], tmp_path)
|
||||
assert cmd == ["/usr/bin/agent", "--force", "--continue"]
|
||||
|
||||
def test_last_flag_maps_to_continue(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["--last"], tmp_path)
|
||||
assert cmd == ["/usr/bin/agent", "--force", "--continue"]
|
||||
|
||||
|
||||
class TestCursorListSessions:
|
||||
"""``myagents cursor --list`` reads ~/.cursor/chats/<cwd-md5>/."""
|
||||
|
||||
def test_list_empty_directory(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with patch(
|
||||
"myagents.launcher._BACKENDS",
|
||||
{
|
||||
"cursor": {
|
||||
**myagents.launcher._BACKENDS["cursor"],
|
||||
"sessions_root": lambda: tmp_path / "chats",
|
||||
}
|
||||
},
|
||||
):
|
||||
result = runner.invoke(
|
||||
cli, ["cursor", "--cwd", str(tmp_path), "--list"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "no sessions" in result.output.lower()
|
||||
|
||||
def test_list_shows_session_id_and_name(self, tmp_path: Path) -> None:
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
cwd = tmp_path / "proj"
|
||||
cwd.mkdir()
|
||||
chats_root = tmp_path / "chats"
|
||||
munged = hashlib.md5(str(cwd.resolve()).encode("utf-8")).hexdigest()
|
||||
session_dir = chats_root / munged / "chat-abc123"
|
||||
session_dir.mkdir(parents=True)
|
||||
db_path = session_dir / "store.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)")
|
||||
conn.execute("CREATE TABLE blobs (id TEXT PRIMARY KEY, data BLOB)")
|
||||
meta = {
|
||||
"agentId": "chat-abc123",
|
||||
"name": "Fix mycursor entrypoint",
|
||||
"mode": "default",
|
||||
"createdAt": 1,
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO meta (key, value) VALUES ('0', ?)",
|
||||
(json.dumps(meta).encode().hex(),),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO blobs (id, data) VALUES ('1', ?)",
|
||||
(
|
||||
json.dumps(
|
||||
{
|
||||
"role": "user",
|
||||
"content": "<user_query>\nhello cursor\n</user_query>",
|
||||
}
|
||||
).encode(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
runner = CliRunner()
|
||||
with patch(
|
||||
"myagents.launcher._BACKENDS",
|
||||
{
|
||||
"cursor": {
|
||||
**myagents.launcher._BACKENDS["cursor"],
|
||||
"sessions_root": lambda: chats_root,
|
||||
}
|
||||
},
|
||||
):
|
||||
result = runner.invoke(
|
||||
cli, ["cursor", "--cwd", str(cwd), "--list"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "chat-abc123" in result.output
|
||||
assert "Fix mycursor entrypoint" in result.output
|
||||
assert "mycursor" in result.output
|
||||
|
||||
+46
-19
@@ -1,28 +1,17 @@
|
||||
"""Tests for standalone myclaude / mykimi / mycodex / myhermes entrypoints."""
|
||||
"""Tests for standalone myclaude / mykimi / mycodex / myhermes / mycursor entrypoints."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from myagents.entrypoints import build_xiaohe_cli, claude_cli, codex_cli, hermes_cli, kimi_cli
|
||||
|
||||
|
||||
class TestXiaoheProvider:
|
||||
"""``xiaohe switch provider`` subcommand registration."""
|
||||
|
||||
def test_help_lists_switch(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(build_xiaohe_cli(), ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "switch" in result.output
|
||||
|
||||
def test_help_lists_info(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(build_xiaohe_cli(), ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "info" in result.output
|
||||
|
||||
from myagents.entrypoints import (
|
||||
claude_cli,
|
||||
codex_cli,
|
||||
cursor_cli,
|
||||
hermes_cli,
|
||||
kimi_cli,
|
||||
)
|
||||
|
||||
class TestMyclaudeEntrypoint:
|
||||
"""``myclaude`` standalone entrypoint."""
|
||||
@@ -161,6 +150,44 @@ class TestMyhermesEntrypoint:
|
||||
]
|
||||
|
||||
|
||||
class TestMycursorEntrypoint:
|
||||
"""``mycursor`` standalone entrypoint."""
|
||||
|
||||
def test_runs_agent(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/agent"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cursor_cli, [])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/agent", "--force"]
|
||||
|
||||
def test_version_shows_mycursor(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cursor_cli, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "mycursor" in result.output
|
||||
|
||||
def test_passthrough(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/agent"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
cursor_cli, ["--cwd", str(tmp_path), "-r", "abc123"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0][-2:] == ["--resume", "abc123"]
|
||||
|
||||
|
||||
class TestTmuxOption:
|
||||
"""``--tmux`` / ``-t`` wraps the backend in an attachable tmux session."""
|
||||
|
||||
|
||||
@@ -23,4 +23,4 @@ class TestUpgradeCommand:
|
||||
with patch.object(up_mod.shutil, "which", return_value=None):
|
||||
result = CliRunner().invoke(up_mod.upgrade_cmd, [])
|
||||
assert result.exit_code != 0
|
||||
assert "xiaohe upgrade" in result.output
|
||||
assert "Install xiaohe-api" in result.output or "xiaohe upgrade" in result.output
|
||||
|
||||
@@ -7,12 +7,6 @@ from myagents.commands import version as ver_mod
|
||||
|
||||
|
||||
class TestDescribeTree:
|
||||
def test_legacy_runtime_tree(self) -> None:
|
||||
tree = Path("/home/u/.xiaohe/runtime/v1.2.3/contrib/myagents")
|
||||
mode, detail = ver_mod.describe_tree(tree)
|
||||
assert mode == "legacy-runtime"
|
||||
assert detail == "v1.2.3"
|
||||
|
||||
def test_development_checkout(self, tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
|
||||
Reference in New Issue
Block a user