repl-toolkit 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- repl_toolkit/__init__.py +70 -0
- repl_toolkit/actions/__init__.py +24 -0
- repl_toolkit/actions/action.py +223 -0
- repl_toolkit/actions/registry.py +564 -0
- repl_toolkit/async_repl.py +374 -0
- repl_toolkit/completion/__init__.py +15 -0
- repl_toolkit/completion/prefix.py +109 -0
- repl_toolkit/completion/shell_expansion.py +453 -0
- repl_toolkit/formatting.py +152 -0
- repl_toolkit/headless_repl.py +251 -0
- repl_toolkit/ptypes.py +122 -0
- repl_toolkit/tests/__init__.py +5 -0
- repl_toolkit/tests/conftest.py +79 -0
- repl_toolkit/tests/test_actions.py +578 -0
- repl_toolkit/tests/test_async_repl.py +381 -0
- repl_toolkit/tests/test_completion.py +656 -0
- repl_toolkit/tests/test_formatting.py +232 -0
- repl_toolkit/tests/test_headless.py +677 -0
- repl_toolkit/tests/test_types.py +174 -0
- repl_toolkit-1.2.0.dist-info/METADATA +761 -0
- repl_toolkit-1.2.0.dist-info/RECORD +24 -0
- repl_toolkit-1.2.0.dist-info/WHEEL +5 -0
- repl_toolkit-1.2.0.dist-info/licenses/LICENSE +21 -0
- repl_toolkit-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for headless mode functionality with action framework support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from repl_toolkit.actions import Action, ActionRegistry
|
|
12
|
+
from repl_toolkit.headless_repl import HeadlessREPL, run_headless_mode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MockAsyncBackend:
|
|
16
|
+
"""Mock backend for headless testing."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, should_succeed=True):
|
|
19
|
+
self.should_succeed = should_succeed
|
|
20
|
+
self.inputs_received = []
|
|
21
|
+
self.call_count = 0
|
|
22
|
+
|
|
23
|
+
async def handle_input(self, user_input: str) -> bool:
|
|
24
|
+
self.inputs_received.append(user_input)
|
|
25
|
+
self.call_count += 1
|
|
26
|
+
# Simulate some processing time
|
|
27
|
+
await asyncio.sleep(0.01)
|
|
28
|
+
return self.should_succeed
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestHeadlessREPL:
|
|
32
|
+
"""Test HeadlessREPL class functionality."""
|
|
33
|
+
|
|
34
|
+
def setup_method(self):
|
|
35
|
+
"""Set up test fixtures."""
|
|
36
|
+
self.backend = MockAsyncBackend()
|
|
37
|
+
self.action_registry = ActionRegistry()
|
|
38
|
+
|
|
39
|
+
def test_headless_repl_initialization(self):
|
|
40
|
+
"""Test HeadlessREPL initialization."""
|
|
41
|
+
repl = HeadlessREPL()
|
|
42
|
+
|
|
43
|
+
assert repl.buffer == ""
|
|
44
|
+
assert repl.send_count == 0
|
|
45
|
+
assert repl.total_success is True
|
|
46
|
+
assert repl.running is True
|
|
47
|
+
assert isinstance(repl.action_registry, ActionRegistry)
|
|
48
|
+
|
|
49
|
+
def test_headless_repl_with_custom_registry(self):
|
|
50
|
+
"""Test HeadlessREPL with custom action registry."""
|
|
51
|
+
custom_registry = ActionRegistry()
|
|
52
|
+
repl = HeadlessREPL(action_registry=custom_registry)
|
|
53
|
+
|
|
54
|
+
assert repl.action_registry is custom_registry
|
|
55
|
+
|
|
56
|
+
def test_add_to_buffer(self):
|
|
57
|
+
"""Test buffer management."""
|
|
58
|
+
repl = HeadlessREPL()
|
|
59
|
+
|
|
60
|
+
# Add first line
|
|
61
|
+
repl._add_to_buffer("Line 1")
|
|
62
|
+
assert repl.buffer == "Line 1"
|
|
63
|
+
|
|
64
|
+
# Add second line
|
|
65
|
+
repl._add_to_buffer("Line 2")
|
|
66
|
+
assert repl.buffer == "Line 1\nLine 2"
|
|
67
|
+
|
|
68
|
+
# Add empty line
|
|
69
|
+
repl._add_to_buffer("")
|
|
70
|
+
assert repl.buffer == "Line 1\nLine 2\n"
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_execute_send_with_content(self):
|
|
74
|
+
"""Test /send execution with buffer content."""
|
|
75
|
+
repl = HeadlessREPL()
|
|
76
|
+
repl._add_to_buffer("Test content")
|
|
77
|
+
|
|
78
|
+
await repl._execute_send(self.backend, "test")
|
|
79
|
+
|
|
80
|
+
assert repl.send_count == 1
|
|
81
|
+
assert repl.buffer == "" # Buffer cleared after send
|
|
82
|
+
assert self.backend.inputs_received == ["Test content"]
|
|
83
|
+
assert repl.total_success is True
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_execute_send_empty_buffer(self):
|
|
87
|
+
"""Test /send execution with empty buffer."""
|
|
88
|
+
repl = HeadlessREPL()
|
|
89
|
+
|
|
90
|
+
await repl._execute_send(self.backend, "test")
|
|
91
|
+
|
|
92
|
+
assert repl.send_count == 0 # No send occurred
|
|
93
|
+
assert repl.buffer == ""
|
|
94
|
+
assert self.backend.inputs_received == []
|
|
95
|
+
|
|
96
|
+
@pytest.mark.asyncio
|
|
97
|
+
async def test_execute_send_backend_failure(self):
|
|
98
|
+
"""Test /send execution with backend failure."""
|
|
99
|
+
self.backend.should_succeed = False
|
|
100
|
+
repl = HeadlessREPL()
|
|
101
|
+
repl._add_to_buffer("Test content")
|
|
102
|
+
|
|
103
|
+
await repl._execute_send(self.backend, "test")
|
|
104
|
+
|
|
105
|
+
assert repl.send_count == 1
|
|
106
|
+
assert repl.buffer == "" # Buffer still cleared
|
|
107
|
+
assert repl.total_success is False # Marked as failed
|
|
108
|
+
assert self.backend.inputs_received == ["Test content"]
|
|
109
|
+
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
async def test_execute_send_backend_exception(self):
|
|
112
|
+
"""Test /send execution with backend exception."""
|
|
113
|
+
backend = AsyncMock()
|
|
114
|
+
backend.handle_input.side_effect = Exception("Backend error")
|
|
115
|
+
|
|
116
|
+
repl = HeadlessREPL()
|
|
117
|
+
repl._add_to_buffer("Test content")
|
|
118
|
+
|
|
119
|
+
await repl._execute_send(backend, "test")
|
|
120
|
+
|
|
121
|
+
assert repl.send_count == 1
|
|
122
|
+
assert repl.buffer == "" # Buffer cleared even on exception
|
|
123
|
+
assert repl.total_success is False
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_handle_eof_with_content(self):
|
|
127
|
+
"""Test EOF handling with buffer content."""
|
|
128
|
+
repl = HeadlessREPL()
|
|
129
|
+
repl._add_to_buffer("Final content")
|
|
130
|
+
|
|
131
|
+
await repl._handle_eof(self.backend)
|
|
132
|
+
|
|
133
|
+
assert repl.send_count == 1
|
|
134
|
+
assert repl.buffer == ""
|
|
135
|
+
assert self.backend.inputs_received == ["Final content"]
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_handle_eof_empty_buffer(self):
|
|
139
|
+
"""Test EOF handling with empty buffer."""
|
|
140
|
+
repl = HeadlessREPL()
|
|
141
|
+
|
|
142
|
+
await repl._handle_eof(self.backend)
|
|
143
|
+
|
|
144
|
+
assert repl.send_count == 0
|
|
145
|
+
assert self.backend.inputs_received == []
|
|
146
|
+
|
|
147
|
+
def test_execute_command(self):
|
|
148
|
+
"""Test command execution through action system."""
|
|
149
|
+
executed_commands = []
|
|
150
|
+
|
|
151
|
+
def mock_handle_command(command, **kwargs):
|
|
152
|
+
executed_commands.append((command, kwargs))
|
|
153
|
+
|
|
154
|
+
repl = HeadlessREPL()
|
|
155
|
+
repl.action_registry.handle_command = mock_handle_command
|
|
156
|
+
|
|
157
|
+
repl._execute_command("/test arg1 arg2")
|
|
158
|
+
|
|
159
|
+
assert len(executed_commands) == 1
|
|
160
|
+
command, kwargs = executed_commands[0]
|
|
161
|
+
assert command == "/test arg1 arg2"
|
|
162
|
+
assert kwargs["headless_mode"] is True
|
|
163
|
+
assert kwargs["buffer"] == ""
|
|
164
|
+
|
|
165
|
+
def test_execute_command_with_buffer(self):
|
|
166
|
+
"""Test command execution with buffer content."""
|
|
167
|
+
executed_commands = []
|
|
168
|
+
|
|
169
|
+
def mock_handle_command(command, **kwargs):
|
|
170
|
+
executed_commands.append((command, kwargs))
|
|
171
|
+
|
|
172
|
+
repl = HeadlessREPL()
|
|
173
|
+
repl.action_registry.handle_command = mock_handle_command
|
|
174
|
+
repl._add_to_buffer("Buffer content")
|
|
175
|
+
|
|
176
|
+
repl._execute_command("/status")
|
|
177
|
+
|
|
178
|
+
assert len(executed_commands) == 1
|
|
179
|
+
command, kwargs = executed_commands[0]
|
|
180
|
+
assert command == "/status"
|
|
181
|
+
assert kwargs["buffer"] == "Buffer content"
|
|
182
|
+
|
|
183
|
+
def test_execute_command_exception(self):
|
|
184
|
+
"""Test command execution with exception."""
|
|
185
|
+
|
|
186
|
+
def mock_handle_command(command, **kwargs):
|
|
187
|
+
raise Exception("Command error")
|
|
188
|
+
|
|
189
|
+
repl = HeadlessREPL()
|
|
190
|
+
repl.action_registry.handle_command = mock_handle_command
|
|
191
|
+
|
|
192
|
+
# Should not raise exception
|
|
193
|
+
repl._execute_command("/error")
|
|
194
|
+
|
|
195
|
+
# REPL should continue functioning
|
|
196
|
+
assert repl.buffer == ""
|
|
197
|
+
|
|
198
|
+
@pytest.mark.asyncio
|
|
199
|
+
async def test_run_with_initial_message(self):
|
|
200
|
+
"""Test run with initial message."""
|
|
201
|
+
repl = HeadlessREPL()
|
|
202
|
+
|
|
203
|
+
with patch.object(repl, "_stdin_loop") as mock_stdin_loop:
|
|
204
|
+
result = await repl.run(self.backend, "Initial message")
|
|
205
|
+
|
|
206
|
+
assert result is True
|
|
207
|
+
assert repl.action_registry.backend is self.backend
|
|
208
|
+
assert self.backend.inputs_received == ["Initial message"]
|
|
209
|
+
mock_stdin_loop.assert_called_once_with(self.backend)
|
|
210
|
+
|
|
211
|
+
@pytest.mark.asyncio
|
|
212
|
+
async def test_run_without_initial_message(self):
|
|
213
|
+
"""Test run without initial message."""
|
|
214
|
+
repl = HeadlessREPL()
|
|
215
|
+
|
|
216
|
+
with patch.object(repl, "_stdin_loop") as mock_stdin_loop:
|
|
217
|
+
result = await repl.run(self.backend)
|
|
218
|
+
|
|
219
|
+
assert result is True
|
|
220
|
+
assert self.backend.inputs_received == []
|
|
221
|
+
mock_stdin_loop.assert_called_once_with(self.backend)
|
|
222
|
+
|
|
223
|
+
@pytest.mark.asyncio
|
|
224
|
+
async def test_run_initial_message_failure(self):
|
|
225
|
+
"""Test run with initial message backend failure."""
|
|
226
|
+
self.backend.should_succeed = False
|
|
227
|
+
repl = HeadlessREPL()
|
|
228
|
+
|
|
229
|
+
with patch.object(repl, "_stdin_loop") as mock_stdin_loop:
|
|
230
|
+
result = await repl.run(self.backend, "Initial message")
|
|
231
|
+
|
|
232
|
+
assert result is False # Overall failure due to initial message
|
|
233
|
+
assert self.backend.inputs_received == ["Initial message"]
|
|
234
|
+
mock_stdin_loop.assert_called_once_with(self.backend)
|
|
235
|
+
|
|
236
|
+
@pytest.mark.asyncio
|
|
237
|
+
async def test_run_exception_handling(self):
|
|
238
|
+
"""Test run with exception in stdin loop."""
|
|
239
|
+
repl = HeadlessREPL()
|
|
240
|
+
|
|
241
|
+
def mock_stdin_loop(backend):
|
|
242
|
+
raise Exception("stdin error")
|
|
243
|
+
|
|
244
|
+
with patch.object(repl, "_stdin_loop", side_effect=mock_stdin_loop):
|
|
245
|
+
result = await repl.run(self.backend)
|
|
246
|
+
|
|
247
|
+
assert result is False
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class TestStdinProcessing:
|
|
251
|
+
"""Test stdin processing functionality."""
|
|
252
|
+
|
|
253
|
+
def setup_method(self):
|
|
254
|
+
"""Set up test fixtures."""
|
|
255
|
+
self.backend = MockAsyncBackend()
|
|
256
|
+
|
|
257
|
+
@pytest.mark.asyncio
|
|
258
|
+
async def test_stdin_loop_simple_send(self):
|
|
259
|
+
"""Test stdin loop with simple content and send."""
|
|
260
|
+
stdin_input = "Line 1\nLine 2\n/send\n"
|
|
261
|
+
|
|
262
|
+
repl = HeadlessREPL()
|
|
263
|
+
|
|
264
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
265
|
+
await repl._stdin_loop(self.backend)
|
|
266
|
+
|
|
267
|
+
assert repl.send_count == 1
|
|
268
|
+
assert repl.buffer == "" # Cleared after send
|
|
269
|
+
assert self.backend.inputs_received == ["Line 1\nLine 2"]
|
|
270
|
+
|
|
271
|
+
@pytest.mark.asyncio
|
|
272
|
+
async def test_stdin_loop_multiple_sends(self):
|
|
273
|
+
"""Test stdin loop with multiple send cycles."""
|
|
274
|
+
stdin_input = "Line 1\nLine 2\n/send\nLine 3\nLine 4\n/send\n"
|
|
275
|
+
|
|
276
|
+
repl = HeadlessREPL()
|
|
277
|
+
|
|
278
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
279
|
+
await repl._stdin_loop(self.backend)
|
|
280
|
+
|
|
281
|
+
assert repl.send_count == 2
|
|
282
|
+
assert repl.buffer == ""
|
|
283
|
+
assert self.backend.inputs_received == ["Line 1\nLine 2", "Line 3\nLine 4"]
|
|
284
|
+
|
|
285
|
+
@pytest.mark.asyncio
|
|
286
|
+
async def test_stdin_loop_eof_with_content(self):
|
|
287
|
+
"""Test stdin loop with EOF and remaining content."""
|
|
288
|
+
stdin_input = "Line 1\nLine 2\n" # No /send, just EOF
|
|
289
|
+
|
|
290
|
+
repl = HeadlessREPL()
|
|
291
|
+
|
|
292
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
293
|
+
await repl._stdin_loop(self.backend)
|
|
294
|
+
|
|
295
|
+
assert repl.send_count == 1 # EOF triggered send
|
|
296
|
+
assert repl.buffer == ""
|
|
297
|
+
assert self.backend.inputs_received == ["Line 1\nLine 2"]
|
|
298
|
+
|
|
299
|
+
@pytest.mark.asyncio
|
|
300
|
+
async def test_stdin_loop_commands_between_content(self):
|
|
301
|
+
"""Test stdin loop with commands between content."""
|
|
302
|
+
stdin_input = "Line 1\n/help\nLine 2\n/send\n"
|
|
303
|
+
|
|
304
|
+
executed_commands = []
|
|
305
|
+
|
|
306
|
+
def mock_handle_command(command, **kwargs):
|
|
307
|
+
executed_commands.append(command)
|
|
308
|
+
|
|
309
|
+
repl = HeadlessREPL()
|
|
310
|
+
repl.action_registry.handle_command = mock_handle_command
|
|
311
|
+
|
|
312
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
313
|
+
await repl._stdin_loop(self.backend)
|
|
314
|
+
|
|
315
|
+
assert repl.send_count == 1
|
|
316
|
+
assert self.backend.inputs_received == ["Line 1\nLine 2"]
|
|
317
|
+
assert executed_commands == ["/help"]
|
|
318
|
+
|
|
319
|
+
@pytest.mark.asyncio
|
|
320
|
+
async def test_stdin_loop_empty_lines(self):
|
|
321
|
+
"""Test stdin loop with empty lines."""
|
|
322
|
+
stdin_input = "Line 1\n\nLine 2\n/send\n"
|
|
323
|
+
|
|
324
|
+
repl = HeadlessREPL()
|
|
325
|
+
|
|
326
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
327
|
+
await repl._stdin_loop(self.backend)
|
|
328
|
+
|
|
329
|
+
assert self.backend.inputs_received == ["Line 1\n\nLine 2"]
|
|
330
|
+
|
|
331
|
+
@pytest.mark.asyncio
|
|
332
|
+
async def test_stdin_loop_only_commands(self):
|
|
333
|
+
"""Test stdin loop with only commands, no content."""
|
|
334
|
+
stdin_input = "/help\n/status\n/send\n"
|
|
335
|
+
|
|
336
|
+
executed_commands = []
|
|
337
|
+
|
|
338
|
+
def mock_handle_command(command, **kwargs):
|
|
339
|
+
executed_commands.append(command)
|
|
340
|
+
|
|
341
|
+
repl = HeadlessREPL()
|
|
342
|
+
repl.action_registry.handle_command = mock_handle_command
|
|
343
|
+
|
|
344
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
345
|
+
await repl._stdin_loop(self.backend)
|
|
346
|
+
|
|
347
|
+
assert repl.send_count == 0 # No send because buffer was empty
|
|
348
|
+
assert self.backend.inputs_received == []
|
|
349
|
+
assert executed_commands == ["/help", "/status"]
|
|
350
|
+
|
|
351
|
+
@pytest.mark.asyncio
|
|
352
|
+
async def test_stdin_loop_keyboard_interrupt(self):
|
|
353
|
+
"""Test stdin loop with KeyboardInterrupt - should NOT send buffer."""
|
|
354
|
+
repl = HeadlessREPL()
|
|
355
|
+
repl._add_to_buffer("Interrupted content")
|
|
356
|
+
|
|
357
|
+
def mock_readline():
|
|
358
|
+
raise KeyboardInterrupt()
|
|
359
|
+
|
|
360
|
+
with patch("sys.stdin.readline", side_effect=mock_readline):
|
|
361
|
+
# KeyboardInterrupt should be caught and handled gracefully
|
|
362
|
+
try:
|
|
363
|
+
await repl._stdin_loop(self.backend)
|
|
364
|
+
except KeyboardInterrupt:
|
|
365
|
+
pass # Expected behavior - KeyboardInterrupt propagates
|
|
366
|
+
|
|
367
|
+
# KeyboardInterrupt should NOT trigger EOF handling or send buffer
|
|
368
|
+
assert repl.send_count == 0 # No sends should occur
|
|
369
|
+
assert self.backend.inputs_received == [] # No content should be sent
|
|
370
|
+
assert repl.buffer == "Interrupted content" # Buffer should remain intact
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class TestRunHeadlessMode:
|
|
374
|
+
"""Test run_headless_mode convenience function."""
|
|
375
|
+
|
|
376
|
+
def setup_method(self):
|
|
377
|
+
"""Set up test fixtures."""
|
|
378
|
+
self.backend = MockAsyncBackend()
|
|
379
|
+
|
|
380
|
+
@pytest.mark.asyncio
|
|
381
|
+
async def test_run_headless_mode_basic(self):
|
|
382
|
+
"""Test basic run_headless_mode functionality."""
|
|
383
|
+
with patch("repl_toolkit.headless_repl.HeadlessREPL") as mock_repl_class:
|
|
384
|
+
mock_repl = Mock()
|
|
385
|
+
mock_repl.run = AsyncMock(return_value=True)
|
|
386
|
+
mock_repl_class.return_value = mock_repl
|
|
387
|
+
|
|
388
|
+
result = await run_headless_mode(backend=self.backend, initial_message="test message")
|
|
389
|
+
|
|
390
|
+
assert result is True
|
|
391
|
+
mock_repl_class.assert_called_once_with(None) # No custom registry
|
|
392
|
+
mock_repl.run.assert_called_once_with(self.backend, "test message")
|
|
393
|
+
|
|
394
|
+
@pytest.mark.asyncio
|
|
395
|
+
async def test_run_headless_mode_with_registry(self):
|
|
396
|
+
"""Test run_headless_mode with custom action registry."""
|
|
397
|
+
custom_registry = ActionRegistry()
|
|
398
|
+
|
|
399
|
+
with patch("repl_toolkit.headless_repl.HeadlessREPL") as mock_repl_class:
|
|
400
|
+
mock_repl = Mock()
|
|
401
|
+
mock_repl.run = AsyncMock(return_value=True)
|
|
402
|
+
mock_repl_class.return_value = mock_repl
|
|
403
|
+
|
|
404
|
+
result = await run_headless_mode(
|
|
405
|
+
backend=self.backend,
|
|
406
|
+
action_registry=custom_registry,
|
|
407
|
+
initial_message="test message",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
assert result is True
|
|
411
|
+
mock_repl_class.assert_called_once_with(custom_registry)
|
|
412
|
+
mock_repl.run.assert_called_once_with(self.backend, "test message")
|
|
413
|
+
|
|
414
|
+
@pytest.mark.asyncio
|
|
415
|
+
async def test_run_headless_mode_no_initial_message(self):
|
|
416
|
+
"""Test run_headless_mode without initial message."""
|
|
417
|
+
with patch("repl_toolkit.headless_repl.HeadlessREPL") as mock_repl_class:
|
|
418
|
+
mock_repl = Mock()
|
|
419
|
+
mock_repl.run = AsyncMock(return_value=True)
|
|
420
|
+
mock_repl_class.return_value = mock_repl
|
|
421
|
+
|
|
422
|
+
result = await run_headless_mode(backend=self.backend)
|
|
423
|
+
|
|
424
|
+
assert result is True
|
|
425
|
+
mock_repl.run.assert_called_once_with(self.backend, None)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class TestActionIntegration:
|
|
429
|
+
"""Test integration with action system."""
|
|
430
|
+
|
|
431
|
+
def setup_method(self):
|
|
432
|
+
"""Set up test fixtures."""
|
|
433
|
+
self.backend = MockAsyncBackend()
|
|
434
|
+
self.executed_actions = []
|
|
435
|
+
|
|
436
|
+
# Create custom action for testing
|
|
437
|
+
def test_handler(context):
|
|
438
|
+
self.executed_actions.append(
|
|
439
|
+
{
|
|
440
|
+
"name": "test_action",
|
|
441
|
+
"args": context.args,
|
|
442
|
+
"triggered_by": context.triggered_by,
|
|
443
|
+
"headless_mode": getattr(context, "headless_mode", False),
|
|
444
|
+
"buffer": getattr(context, "buffer", None),
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
self.test_action = Action(
|
|
449
|
+
name="test_action",
|
|
450
|
+
description="Test action for headless",
|
|
451
|
+
category="Test",
|
|
452
|
+
handler=test_handler,
|
|
453
|
+
command="/test",
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def test_action_execution_in_headless_mode(self):
|
|
457
|
+
"""Test action execution with headless context."""
|
|
458
|
+
registry = ActionRegistry()
|
|
459
|
+
registry.register_action(self.test_action)
|
|
460
|
+
|
|
461
|
+
repl = HeadlessREPL(action_registry=registry)
|
|
462
|
+
repl._add_to_buffer("Buffer content")
|
|
463
|
+
|
|
464
|
+
repl._execute_command("/test arg1 arg2")
|
|
465
|
+
|
|
466
|
+
assert len(self.executed_actions) == 1
|
|
467
|
+
action_data = self.executed_actions[0]
|
|
468
|
+
assert action_data["name"] == "test_action"
|
|
469
|
+
assert action_data["args"] == ["arg1", "arg2"]
|
|
470
|
+
assert action_data["triggered_by"] == "command"
|
|
471
|
+
assert action_data["headless_mode"] is True
|
|
472
|
+
assert action_data["buffer"] == "Buffer content"
|
|
473
|
+
|
|
474
|
+
@pytest.mark.asyncio
|
|
475
|
+
async def test_action_with_buffer_manipulation(self):
|
|
476
|
+
"""Test action that manipulates the buffer."""
|
|
477
|
+
|
|
478
|
+
def buffer_action_handler(context):
|
|
479
|
+
# Action that adds to buffer (if it has access)
|
|
480
|
+
if hasattr(context, "buffer") and context.buffer:
|
|
481
|
+
# In real implementation, actions might modify buffer
|
|
482
|
+
# For test, just record what they received
|
|
483
|
+
self.executed_actions.append(
|
|
484
|
+
{
|
|
485
|
+
"buffer_content": context.buffer,
|
|
486
|
+
"headless_mode": getattr(context, "headless_mode", False),
|
|
487
|
+
}
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
buffer_action = Action(
|
|
491
|
+
name="buffer_action",
|
|
492
|
+
description="Buffer manipulation action",
|
|
493
|
+
category="Test",
|
|
494
|
+
handler=buffer_action_handler,
|
|
495
|
+
command="/buffer",
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
registry = ActionRegistry()
|
|
499
|
+
registry.register_action(buffer_action)
|
|
500
|
+
|
|
501
|
+
repl = HeadlessREPL(action_registry=registry)
|
|
502
|
+
repl._add_to_buffer("Initial content")
|
|
503
|
+
|
|
504
|
+
repl._execute_command("/buffer")
|
|
505
|
+
|
|
506
|
+
assert len(self.executed_actions) == 1
|
|
507
|
+
assert self.executed_actions[0]["buffer_content"] == "Initial content"
|
|
508
|
+
assert self.executed_actions[0]["headless_mode"] is True
|
|
509
|
+
|
|
510
|
+
@pytest.mark.asyncio
|
|
511
|
+
async def test_builtin_actions_in_headless(self):
|
|
512
|
+
"""Test built-in actions work in headless mode."""
|
|
513
|
+
repl = HeadlessREPL()
|
|
514
|
+
|
|
515
|
+
# Test help action (should not raise exception)
|
|
516
|
+
repl._execute_command("/help")
|
|
517
|
+
|
|
518
|
+
# Test shortcuts action (should not raise exception)
|
|
519
|
+
repl._execute_command("/shortcuts")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class TestErrorHandling:
|
|
523
|
+
"""Test error handling in headless mode."""
|
|
524
|
+
|
|
525
|
+
def setup_method(self):
|
|
526
|
+
"""Set up test fixtures."""
|
|
527
|
+
self.backend = MockAsyncBackend()
|
|
528
|
+
|
|
529
|
+
@pytest.mark.asyncio
|
|
530
|
+
async def test_backend_error_recovery(self):
|
|
531
|
+
"""Test recovery from backend errors."""
|
|
532
|
+
# Backend fails on first call, succeeds on second
|
|
533
|
+
call_count = 0
|
|
534
|
+
|
|
535
|
+
async def failing_handle_input(user_input):
|
|
536
|
+
nonlocal call_count
|
|
537
|
+
call_count += 1
|
|
538
|
+
if call_count == 1:
|
|
539
|
+
return False # First call fails
|
|
540
|
+
return True # Second call succeeds
|
|
541
|
+
|
|
542
|
+
backend = Mock()
|
|
543
|
+
backend.handle_input = failing_handle_input
|
|
544
|
+
|
|
545
|
+
stdin_input = "Content 1\n/send\nContent 2\n/send\n"
|
|
546
|
+
|
|
547
|
+
repl = HeadlessREPL()
|
|
548
|
+
|
|
549
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
550
|
+
await repl._stdin_loop(backend)
|
|
551
|
+
|
|
552
|
+
assert repl.send_count == 2
|
|
553
|
+
assert repl.total_success is False # One failure makes overall false
|
|
554
|
+
|
|
555
|
+
@pytest.mark.asyncio
|
|
556
|
+
async def test_command_error_recovery(self):
|
|
557
|
+
"""Test recovery from command execution errors."""
|
|
558
|
+
|
|
559
|
+
def failing_command_handler(command, **kwargs):
|
|
560
|
+
if "fail" in command:
|
|
561
|
+
raise Exception("Command failed")
|
|
562
|
+
|
|
563
|
+
repl = HeadlessREPL()
|
|
564
|
+
repl.action_registry.handle_command = failing_command_handler
|
|
565
|
+
|
|
566
|
+
stdin_input = "Content 1\n/fail\nContent 2\n/send\n"
|
|
567
|
+
|
|
568
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
569
|
+
await repl._stdin_loop(self.backend)
|
|
570
|
+
|
|
571
|
+
# Should continue processing despite command error
|
|
572
|
+
assert repl.send_count == 1
|
|
573
|
+
assert self.backend.inputs_received == ["Content 1\nContent 2"]
|
|
574
|
+
|
|
575
|
+
@pytest.mark.asyncio
|
|
576
|
+
async def test_multiple_backend_failures(self):
|
|
577
|
+
"""Test handling multiple backend failures."""
|
|
578
|
+
self.backend.should_succeed = False
|
|
579
|
+
|
|
580
|
+
stdin_input = "Content 1\n/send\nContent 2\n/send\nContent 3\n"
|
|
581
|
+
|
|
582
|
+
repl = HeadlessREPL()
|
|
583
|
+
|
|
584
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
585
|
+
await repl._stdin_loop(self.backend)
|
|
586
|
+
|
|
587
|
+
assert repl.send_count == 3 # All sends attempted
|
|
588
|
+
assert repl.total_success is False
|
|
589
|
+
assert len(self.backend.inputs_received) == 3
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class TestComplexScenarios:
|
|
593
|
+
"""Test complex real-world scenarios."""
|
|
594
|
+
|
|
595
|
+
def setup_method(self):
|
|
596
|
+
"""Set up test fixtures."""
|
|
597
|
+
self.backend = MockAsyncBackend()
|
|
598
|
+
|
|
599
|
+
@pytest.mark.asyncio
|
|
600
|
+
async def test_mixed_content_and_commands(self):
|
|
601
|
+
"""Test mixed content and commands scenario."""
|
|
602
|
+
stdin_input = """First line
|
|
603
|
+
/help
|
|
604
|
+
Second line
|
|
605
|
+
/status
|
|
606
|
+
Third line
|
|
607
|
+
/send
|
|
608
|
+
Fourth line
|
|
609
|
+
Fifth line
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
executed_commands = []
|
|
613
|
+
|
|
614
|
+
def mock_handle_command(command, **kwargs):
|
|
615
|
+
executed_commands.append(command)
|
|
616
|
+
|
|
617
|
+
repl = HeadlessREPL()
|
|
618
|
+
repl.action_registry.handle_command = mock_handle_command
|
|
619
|
+
|
|
620
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
621
|
+
await repl._stdin_loop(self.backend)
|
|
622
|
+
|
|
623
|
+
# Should have two sends: one explicit, one EOF
|
|
624
|
+
assert repl.send_count == 2
|
|
625
|
+
assert self.backend.inputs_received == [
|
|
626
|
+
"First line\nSecond line\nThird line",
|
|
627
|
+
"Fourth line\nFifth line",
|
|
628
|
+
]
|
|
629
|
+
assert executed_commands == ["/help", "/status"]
|
|
630
|
+
|
|
631
|
+
@pytest.mark.asyncio
|
|
632
|
+
async def test_empty_sends_and_content(self):
|
|
633
|
+
"""Test scenario with empty sends and content."""
|
|
634
|
+
stdin_input = "/send\nContent\n/send\n/send\nMore content\n"
|
|
635
|
+
|
|
636
|
+
repl = HeadlessREPL()
|
|
637
|
+
|
|
638
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
639
|
+
await repl._stdin_loop(self.backend)
|
|
640
|
+
|
|
641
|
+
# Only 2 actual sends (empty buffer sends are skipped)
|
|
642
|
+
assert repl.send_count == 2
|
|
643
|
+
assert self.backend.inputs_received == ["Content", "More content"]
|
|
644
|
+
|
|
645
|
+
@pytest.mark.asyncio
|
|
646
|
+
async def test_large_content_blocks(self):
|
|
647
|
+
"""Test handling of large content blocks."""
|
|
648
|
+
# Create large content block
|
|
649
|
+
large_content = "\n".join([f"Line {i}" for i in range(1000)])
|
|
650
|
+
stdin_input = f"{large_content}\n/send\n"
|
|
651
|
+
|
|
652
|
+
repl = HeadlessREPL()
|
|
653
|
+
|
|
654
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
655
|
+
await repl._stdin_loop(self.backend)
|
|
656
|
+
|
|
657
|
+
assert repl.send_count == 1
|
|
658
|
+
assert len(self.backend.inputs_received) == 1
|
|
659
|
+
assert len(self.backend.inputs_received[0].split("\n")) == 1000
|
|
660
|
+
|
|
661
|
+
@pytest.mark.asyncio
|
|
662
|
+
async def test_rapid_send_cycles(self):
|
|
663
|
+
"""Test rapid send cycles."""
|
|
664
|
+
# Multiple quick send cycles
|
|
665
|
+
stdin_input = ""
|
|
666
|
+
for i in range(10):
|
|
667
|
+
stdin_input += f"Content {i}\n/send\n"
|
|
668
|
+
|
|
669
|
+
repl = HeadlessREPL()
|
|
670
|
+
|
|
671
|
+
with patch("sys.stdin", StringIO(stdin_input)):
|
|
672
|
+
await repl._stdin_loop(self.backend)
|
|
673
|
+
|
|
674
|
+
assert repl.send_count == 10
|
|
675
|
+
assert len(self.backend.inputs_received) == 10
|
|
676
|
+
for i in range(10):
|
|
677
|
+
assert self.backend.inputs_received[i] == f"Content {i}"
|