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.
@@ -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}"