Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

336 lines
10 KiB
Python

"""Tests for Cursor Agent CLI adapter."""
from __future__ import annotations
import os
from subprocess import TimeoutExpired
from unittest.mock import MagicMock, patch
import pytest
from integrations.llm_cli.cursor import CursorAdapter
def _version_proc() -> MagicMock:
proc = MagicMock()
proc.returncode = 0
proc.stdout = "2026.04.29-c83a488\n"
proc.stderr = ""
return proc
def _status_proc(text: str, returncode: int = 0) -> MagicMock:
proc = MagicMock()
proc.returncode = returncode
proc.stdout = text
proc.stderr = ""
return proc
def _fallback_proc() -> MagicMock:
proc = MagicMock()
proc.returncode = 0
proc.stdout = ""
proc.stderr = ""
return proc
@patch("integrations.llm_cli.cursor.subprocess.run")
@patch("integrations.llm_cli.binary_resolver.shutil.which")
def test_detect_not_logged_in_status_with_cursor_key_stays_not_logged_in(
mock_which: MagicMock, mock_run: MagicMock
) -> None:
mock_which.return_value = "agent"
def side_effect(args, **kwargs):
if "--version" in args:
return _version_proc()
if "status" in args:
return _status_proc("Not logged in\n", returncode=1)
return _fallback_proc()
mock_run.side_effect = side_effect
with patch.dict(
os.environ,
{"CURSOR_BIN": "agent", "CURSOR_API_KEY": "cursor-key", "USERPROFILE": r"C:\Users\test"},
clear=True,
):
probe = CursorAdapter().detect()
assert probe.installed is True
assert probe.logged_in is False
assert "agent login" in probe.detail
@patch("integrations.llm_cli.cursor.subprocess.run")
@patch("integrations.llm_cli.binary_resolver.shutil.which")
def test_detect_unclear_status_with_cursor_key_returns_logged_in(
mock_which: MagicMock, mock_run: MagicMock
) -> None:
mock_which.return_value = "agent"
def side_effect(args, **kwargs):
if "--version" in args:
return _version_proc()
if "status" in args:
return _status_proc("", returncode=0)
return _fallback_proc()
mock_run.side_effect = side_effect
with patch.dict(
os.environ,
{"CURSOR_BIN": "agent", "CURSOR_API_KEY": "cursor-key", "USERPROFILE": r"C:\Users\test"},
clear=True,
):
probe = CursorAdapter().detect()
assert probe.installed is True
assert probe.logged_in is True
@patch("integrations.llm_cli.cursor.subprocess.run")
@patch("integrations.llm_cli.binary_resolver.shutil.which")
def test_detect_logged_in_status_ignores_cursor_key(
mock_which: MagicMock, mock_run: MagicMock
) -> None:
mock_which.return_value = "agent"
def side_effect(args, **kwargs):
if "--version" in args:
return _version_proc()
if "status" in args:
return _status_proc("✓ Logged in as user@example.com\n")
return _fallback_proc()
mock_run.side_effect = side_effect
with patch.dict(
os.environ,
{"CURSOR_BIN": "agent", "CURSOR_API_KEY": "cursor-key", "USERPROFILE": r"C:\Users\test"},
clear=True,
):
probe = CursorAdapter().detect()
assert probe.installed is True
assert probe.logged_in is True
assert "Logged in as" in probe.detail
@patch("integrations.llm_cli.cursor.subprocess.run")
@patch("integrations.llm_cli.binary_resolver.shutil.which")
def test_detect_logged_in_via_status(mock_which: MagicMock, mock_run: MagicMock) -> None:
mock_which.return_value = "agent"
def side_effect(args, **kwargs):
if "--version" in args:
return _version_proc()
if "status" in args:
return _status_proc("✓ Logged in as user@example.com\n")
return _fallback_proc()
mock_run.side_effect = side_effect
with patch.dict(
os.environ,
{"CURSOR_BIN": "agent", "USERPROFILE": r"C:\Users\test"},
clear=True,
):
probe = CursorAdapter().detect()
assert probe.installed is True
assert probe.logged_in is True
assert "Logged in as" in probe.detail
@patch("integrations.llm_cli.cursor.subprocess.run")
@patch("integrations.llm_cli.binary_resolver.shutil.which")
def test_detect_not_logged_in(mock_which: MagicMock, mock_run: MagicMock) -> None:
mock_which.return_value = "agent"
def side_effect(args, **kwargs):
if "--version" in args:
return _version_proc()
if "status" in args:
return _status_proc("Not logged in\n", returncode=1)
return _fallback_proc()
mock_run.side_effect = side_effect
with patch.dict(
os.environ,
{"CURSOR_BIN": "agent", "USERPROFILE": r"C:\Users\test"},
clear=True,
):
probe = CursorAdapter().detect()
assert probe.installed is True
assert probe.logged_in is False
@patch("integrations.llm_cli.cursor.subprocess.run")
@patch("integrations.llm_cli.binary_resolver.shutil.which")
def test_detect_status_timeout_without_api_key(mock_which: MagicMock, mock_run: MagicMock) -> None:
mock_which.return_value = "agent"
def side_effect(args, **kwargs):
if "--version" in args:
return _version_proc()
if "status" in args:
raise TimeoutExpired(cmd=args, timeout=kwargs.get("timeout", 0))
return _fallback_proc()
mock_run.side_effect = side_effect
with patch.dict(
os.environ,
{"CURSOR_BIN": "agent", "CURSOR_API_KEY": "", "USERPROFILE": r"C:\Users\test"},
clear=True,
):
probe = CursorAdapter().detect()
assert probe.installed is True
assert probe.logged_in is None
assert "status" in probe.detail
@patch("integrations.llm_cli.cursor.subprocess.run")
@patch("integrations.llm_cli.binary_resolver.shutil.which")
def test_detect_status_timeout_with_api_key(mock_which: MagicMock, mock_run: MagicMock) -> None:
mock_which.return_value = "agent"
def side_effect(args, **kwargs):
if "--version" in args:
return _version_proc()
if "status" in args:
raise TimeoutExpired(cmd=args, timeout=kwargs.get("timeout", 0))
return _fallback_proc()
mock_run.side_effect = side_effect
with patch.dict(
os.environ,
{"CURSOR_BIN": "agent", "CURSOR_API_KEY": "ck", "USERPROFILE": r"C:\Users\test"},
clear=True,
):
probe = CursorAdapter().detect()
assert probe.installed is True
assert probe.logged_in is True
assert "CURSOR_API_KEY" in probe.detail
@patch.dict(os.environ, {"CURSOR_API_KEY": ""}, clear=False)
@patch("integrations.llm_cli.binary_resolver.shutil.which", return_value="agent")
def test_build_adds_trust_workspace_and_model(mock_which: MagicMock) -> None:
inv = CursorAdapter().build(prompt="hello", model="auto", workspace="/tmp/project")
assert inv.stdin == "hello"
assert inv.cwd == "/tmp/project"
assert "--print" in inv.argv
assert "--trust" in inv.argv
assert "--output-format" in inv.argv
assert "text" in inv.argv
assert "--workspace" in inv.argv
assert "/tmp/project" in inv.argv
assert "--model" in inv.argv
assert "auto" in inv.argv
assert inv.env is None
@patch.dict(os.environ, {"CURSOR_API_KEY": "ck-headless"}, clear=False)
@patch("integrations.llm_cli.binary_resolver.shutil.which", return_value="agent")
def test_build_forwards_cursor_api_key(mock_which: MagicMock) -> None:
"""Headless auth must reach the subprocess (same pattern as Codex/OpenCode env overrides)."""
inv = CursorAdapter().build(prompt="hello", model=None, workspace="/tmp/project")
assert inv.env is not None
assert inv.env.get("CURSOR_API_KEY") == "ck-headless"
def test_cursor_cli_registry_entry() -> None:
from integrations.llm_cli.registry import get_cli_provider_registration
reg = get_cli_provider_registration("cursor")
assert reg is not None
assert reg.model_env_key == "CURSOR_MODEL"
assert reg.adapter_factory().name == "cursor"
@patch("integrations.llm_cli.runner.subprocess.run")
def test_cli_backed_client_invoke_forwards_cursor_env(mock_run: MagicMock) -> None:
from integrations.llm_cli.runner import CLIBackedLLMClient
mock_adapter = MagicMock(spec=CursorAdapter)
mock_adapter.name = "cursor"
mock_adapter.detect.return_value = MagicMock(
installed=True,
bin_path="agent",
logged_in=True,
detail="ok",
)
mock_adapter.build.return_value = MagicMock(
argv=["agent", "--print", "--trust"],
stdin="hello",
cwd="/tmp",
env=None,
timeout_sec=30.0,
)
mock_adapter.parse.return_value = "answer"
mock_adapter.explain_failure.return_value = "fail"
mock_run.return_value = MagicMock(returncode=0, stdout="answer\n", stderr="")
with (
patch("platform.guardrails.engine.get_guardrail_engine") as guardrails,
patch.dict(
os.environ,
{
"CURSOR_API_KEY": "cursor-key",
"CURSOR_MODEL": "auto",
"OPENAI_API_KEY": "openai-key",
},
clear=False,
),
):
guardrails.return_value.is_active = False
client = CLIBackedLLMClient(mock_adapter, model="auto", max_tokens=256)
resp = client.invoke("hello")
assert resp.content == "answer"
env = mock_run.call_args.kwargs["env"]
assert env["CURSOR_API_KEY"] == "cursor-key"
assert env["CURSOR_MODEL"] == "auto"
assert "OPENAI_API_KEY" not in env
def test_parse_and_explain_failure() -> None:
adapter = CursorAdapter()
assert adapter.parse(stdout=" Hello! \n", stderr="", returncode=0) == "Hello!"
with pytest.raises(RuntimeError, match="empty output"):
adapter.parse(stdout=" ", stderr="", returncode=0)
auth_failure = adapter.explain_failure(
stdout="",
stderr="Error: Authentication required",
returncode=1,
)
assert "Not logged in" in auth_failure
trust_failure = adapter.explain_failure(
stdout="Workspace Trust Required",
stderr="",
returncode=1,
)
assert "Workspace trust required" in trust_failure
model_failure = adapter.explain_failure(
stdout="Named models unavailable",
stderr="",
returncode=1,
)
assert "CURSOR_MODEL" in model_failure