forked from omnigent-ai/omnigent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_cursor_native.py
More file actions
308 lines (253 loc) · 10.2 KB
/
Copy pathtest_cursor_native.py
File metadata and controls
308 lines (253 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
"""Tests for cursor-native CLI orchestration."""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
import httpx
import pytest
from omnigent import cursor_native
class _FakeAsyncClient:
"""Minimal async client for cursor-native daemon orchestration tests."""
def __init__(self, *, terminal_running: bool) -> None:
self.terminal_running = terminal_running
self.terminal_gets = 0
self.patch_calls: list[tuple[str, dict[str, Any]]] = []
self.post_calls: list[tuple[str, dict[str, Any] | None]] = []
async def __aenter__(self) -> _FakeAsyncClient:
return self
async def __aexit__(self, *args: object) -> None:
return None
async def get(self, url: str) -> httpx.Response:
request = httpx.Request("GET", url)
if url == "/v1/sessions/conv_cursor":
return httpx.Response(
200,
json={"labels": {"omnigent.wrapper": "cursor-native-ui"}},
request=request,
)
if url.endswith("/resources/terminals/terminal_cursor_main"):
self.terminal_gets += 1
if not self.terminal_running and self.terminal_gets == 1:
return httpx.Response(404, request=request)
return httpx.Response(
200,
json={
"id": "terminal_cursor_main",
"metadata": {
"running": True,
"tmux_socket": "/tmp/cursor.sock",
"tmux_target": "cursor:0",
},
},
request=request,
)
raise AssertionError(f"unexpected GET {url}")
async def patch(self, url: str, *, json: dict[str, Any]) -> httpx.Response:
self.patch_calls.append((url, json))
return httpx.Response(200, request=httpx.Request("PATCH", url))
async def post(
self,
url: str,
*,
json: dict[str, Any] | None = None,
**_: object,
) -> httpx.Response:
self.post_calls.append((url, json))
return httpx.Response(200, request=httpx.Request("POST", url))
@pytest.mark.asyncio
async def test_cursor_resume_to_live_terminal_is_marked_as_reattach(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A resume with a still-running terminal is a true live reattach."""
fake = _FakeAsyncClient(terminal_running=True)
monkeypatch.setattr(cursor_native.httpx, "AsyncClient", lambda **_: fake)
prepared = await cursor_native._prepare_cursor_terminal_via_daemon(
base_url="http://server",
headers={},
session_id="conv_cursor",
session_bundle=None,
cursor_args=("-f",),
host_id="host_1",
workspace="/workspace",
)
assert prepared.reattached is True
assert prepared.cold_resumed is False
assert prepared.terminal_id == "terminal_cursor_main"
assert fake.patch_calls == []
assert fake.post_calls == []
@pytest.mark.asyncio
async def test_cursor_resume_without_live_terminal_is_marked_as_cold_resume(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A resume whose terminal is gone cold-starts a fresh Cursor TUI."""
fake = _FakeAsyncClient(terminal_running=False)
monkeypatch.setattr(cursor_native.httpx, "AsyncClient", lambda **_: fake)
monkeypatch.setattr(cursor_native, "wait_for_host_online", _async_noop)
monkeypatch.setattr(cursor_native, "wait_for_runner_online", _async_noop)
monkeypatch.setattr(cursor_native, "launch_or_reuse_daemon_runner", _launch_runner)
monkeypatch.setattr(cursor_native, "_bind_session_runner", _async_noop)
prepared = await cursor_native._prepare_cursor_terminal_via_daemon(
base_url="http://server",
headers={},
session_id="conv_cursor",
session_bundle=None,
cursor_args=("-f",),
host_id="host_1",
workspace="/workspace",
)
assert prepared.reattached is False
assert prepared.cold_resumed is True
assert prepared.terminal_id == "terminal_cursor_main"
assert fake.patch_calls == [("/v1/sessions/conv_cursor", {"terminal_launch_args": ["-f"]})]
assert fake.post_calls == [
(
"/v1/sessions/conv_cursor/resources/terminals",
{
"terminal": "cursor",
"session_key": "main",
"ensure_native_terminal": True,
},
)
]
@pytest.mark.asyncio
async def test_cursor_cold_resume_pins_model(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A model pin on cold resume persists model_override alongside args."""
fake = _FakeAsyncClient(terminal_running=False)
monkeypatch.setattr(cursor_native.httpx, "AsyncClient", lambda **_: fake)
monkeypatch.setattr(cursor_native, "wait_for_host_online", _async_noop)
monkeypatch.setattr(cursor_native, "wait_for_runner_online", _async_noop)
monkeypatch.setattr(cursor_native, "launch_or_reuse_daemon_runner", _launch_runner)
monkeypatch.setattr(cursor_native, "_bind_session_runner", _async_noop)
await cursor_native._prepare_cursor_terminal_via_daemon(
base_url="http://server",
headers={},
session_id="conv_cursor",
session_bundle=None,
cursor_args=("-f",),
model="gpt-5.2",
host_id="host_1",
workspace="/workspace",
)
assert fake.patch_calls == [
("/v1/sessions/conv_cursor", {"terminal_launch_args": ["-f"], "model_override": "gpt-5.2"})
]
_CURSOR_MODELS_OUTPUT = """Available models
auto - Auto (default)
gpt-5.3-codex-low - Codex 5.3 Low
gpt-5.3-codex-high-fast - Codex 5.3 High Fast
gpt-5.1-high - GPT-5.1 High
claude-4.6-opus-high - Opus 4.6 1M
claude-4.6-opus-high-thinking - Opus 4.6 1M Thinking
claude-4-sonnet-thinking - Sonnet 4 Thinking
composer-2.5 - Composer 2.5 (current)
"""
def test_parse_cursor_cli_model_options_normalizes_base_ids() -> None:
"""Live compound variants collapse to injectable base ids in CLI order."""
models = cursor_native.parse_cursor_cli_model_options(_CURSOR_MODELS_OUTPUT)
assert models == [
{"id": "auto", "displayName": "Auto", "isDefault": True, "isCurrent": False},
{
"id": "gpt-5.3-codex",
"displayName": "Codex 5.3",
"isDefault": False,
"isCurrent": False,
},
{
"id": "gpt-5.1",
"displayName": "GPT-5.1",
"isDefault": False,
"isCurrent": False,
},
{
"id": "claude-opus-4-6",
"displayName": "Opus 4.6",
"isDefault": False,
"isCurrent": False,
},
{
"id": "composer-2.5",
"displayName": "Composer 2.5",
"isDefault": False,
"isCurrent": True,
},
]
def test_parse_cursor_cli_model_options_keeps_one_default_and_current() -> None:
"""Conflicting CLI tags resolve deterministically in catalog order."""
models = cursor_native.parse_cursor_cli_model_options(
"""Available models
first-high - First High (default, current)
second-low - Second Low (default, current)
"""
)
assert [model["id"] for model in models if model["isDefault"]] == ["first"]
assert [model["id"] for model in models if model["isCurrent"]] == ["first"]
def test_parse_cursor_cli_model_options_logs_unmapped_claude_ids(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Reversed Claude ids that cannot round-trip never reach the picker."""
models = cursor_native.parse_cursor_cli_model_options(_CURSOR_MODELS_OUTPUT)
assert all(model["id"] != "claude-4-sonnet" for model in models)
assert "Skipping non-injectable Cursor model id 'claude-4-sonnet'" in caplog.text
def test_parse_cursor_cli_model_options_rejects_empty_catalog() -> None:
"""Malformed CLI output is retryable rather than cached as an empty picker."""
with pytest.raises(ValueError, match="did not contain any valid models"):
cursor_native.parse_cursor_cli_model_options("Available models\n")
def test_list_cursor_cli_model_options_runs_configured_binary(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Discovery invokes the resolved CLI and parses its stdout."""
calls: list[dict[str, Any]] = []
monkeypatch.setattr(
cursor_native,
"resolve_cursor_executable",
lambda **_: "/opt/cursor-agent",
)
def run(command: list[str], **kwargs: Any) -> SimpleNamespace:
calls.append({"command": command, **kwargs})
return SimpleNamespace(stdout=_CURSOR_MODELS_OUTPUT)
monkeypatch.setattr(cursor_native.subprocess, "run", run)
models = cursor_native.list_cursor_cli_model_options(env={"HOME": "/tmp/home"}, timeout_s=3.0)
assert models[0]["id"] == "auto"
assert calls == [
{
"command": ["/opt/cursor-agent", "models"],
"check": True,
"capture_output": True,
"text": True,
"timeout": 3.0,
"env": {"HOME": "/tmp/home"},
}
]
async def _async_noop(*_: object, **__: object) -> None:
return None
async def _launch_runner(*_: object, **__: object) -> str:
return "runner_1"
class TestIsValidCursorChatId:
"""``is_valid_cursor_chat_id`` gates the persisted external_session_id."""
@pytest.mark.parametrize(
"chat_id",
[
"0ef42bbf-3b80-4bec-ac39-ca46531cbc47",
"00000000-0000-0000-0000-000000000000",
"0EF42BBF-3B80-4BEC-AC39-CA46531CBC47", # uppercase hex
],
)
def test_accepts_well_formed_uuids(self, chat_id: str) -> None:
assert cursor_native.is_valid_cursor_chat_id(chat_id) is True
@pytest.mark.parametrize(
"chat_id",
[
None,
"",
"deadbeef", # hex but not UUID-shaped
"chat-uuid-abc123", # non-hex letters
"----",
"../../etc/passwd", # path traversal shape
"0ef42bbf;reboot", # shell metachar
"0ef42bbf-3b80-4bec-ac39-ca46531cbc47x", # trailing junk
"0ef42bbf-3b80-4bec-ac39-ca46531cbc4", # one short in last group
],
)
def test_rejects_malformed_or_empty(self, chat_id: str | None) -> None:
assert cursor_native.is_valid_cursor_chat_id(chat_id) is False