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,381 @@
1
+ """
2
+ Tests for AsyncREPL with action support and late backend binding.
3
+ """
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from unittest.mock import AsyncMock, Mock, patch
8
+
9
+ import pytest
10
+ from prompt_toolkit.key_binding import KeyBindings
11
+
12
+ from repl_toolkit import AsyncREPL, run_async_repl
13
+ from repl_toolkit.actions import Action, ActionContext, ActionRegistry
14
+
15
+
16
+ class MockBackend:
17
+ """Mock backend for testing."""
18
+
19
+ def __init__(self):
20
+ self.inputs_received = []
21
+ self.should_succeed = True
22
+
23
+ async def handle_input(self, user_input: str) -> bool:
24
+ self.inputs_received.append(user_input)
25
+ await asyncio.sleep(0.01) # Simulate processing
26
+ return self.should_succeed
27
+
28
+
29
+ class TestAsyncREPL:
30
+ """Test AsyncREPL functionality with late backend binding."""
31
+
32
+ def setup_method(self):
33
+ """Set up test fixtures."""
34
+ self.backend = MockBackend()
35
+ self.action_registry = ActionRegistry()
36
+
37
+ def test_repl_initialization(self, mock_terminal_for_repl):
38
+ """Test REPL initialization without backend."""
39
+ repl = AsyncREPL()
40
+
41
+ # REPL should initialize without backend
42
+ assert repl.action_registry is not None
43
+ assert hasattr(repl.action_registry, "backend")
44
+ # Backend not set until run() is called
45
+ assert repl.action_registry.backend is None
46
+
47
+ def test_repl_with_custom_registry(self, mock_terminal_for_repl):
48
+ """Test REPL with custom action registry."""
49
+ custom_registry = ActionRegistry()
50
+ repl = AsyncREPL(action_registry=custom_registry)
51
+
52
+ assert repl.action_registry is custom_registry
53
+
54
+ def test_repl_with_history(self, mock_terminal_for_repl):
55
+ """Test REPL with history file."""
56
+ history_path = Path("/tmp/test_history.txt")
57
+ repl = AsyncREPL(history_path=history_path)
58
+
59
+ # Should not raise error during initialization
60
+ assert repl.session.history is not None
61
+
62
+ def test_key_parsing(self, mock_terminal_for_repl):
63
+ """Test key combination parsing."""
64
+ repl = AsyncREPL()
65
+
66
+ # Test function keys
67
+ assert repl._parse_key_combination("F1") == ("f1",)
68
+ assert repl._parse_key_combination("F12") == ("f12",)
69
+
70
+ # Test modifier combinations
71
+ assert repl._parse_key_combination("ctrl-s") == ("c-s",)
72
+ assert repl._parse_key_combination("alt-h") == ("escape", "h")
73
+
74
+ # Test single keys
75
+ assert repl._parse_key_combination("enter") == ("enter",)
76
+
77
+ def test_should_exit(self, mock_terminal_for_repl):
78
+ """Test exit condition detection."""
79
+ repl = AsyncREPL()
80
+
81
+ assert repl._should_exit("/exit")
82
+ assert repl._should_exit("/quit")
83
+ assert repl._should_exit(" /EXIT ")
84
+ assert not repl._should_exit("/help")
85
+ assert not repl._should_exit("regular input")
86
+
87
+ def test_backend_injection_during_run(self, mock_terminal_for_repl):
88
+ """Test backend injection into action registry during run."""
89
+ repl = AsyncREPL()
90
+
91
+ # Initially no backend
92
+ assert repl.action_registry.backend is None
93
+
94
+ # Backend should be injected when run() is called
95
+ # We can't easily test the full run() method, but we can test the injection logic
96
+ repl.action_registry.backend = self.backend
97
+ assert repl.action_registry.backend is self.backend
98
+
99
+ @patch("repl_toolkit.async_repl.PromptSession")
100
+ def test_key_bindings_registration(self, mock_session):
101
+ """Test that key bindings are properly registered."""
102
+ # Add a test action with shortcut
103
+ test_action = Action(
104
+ name="test_shortcut",
105
+ description="Test shortcut",
106
+ category="Test",
107
+ handler=lambda ctx: None,
108
+ keys="F5",
109
+ )
110
+ self.action_registry.register_action(test_action)
111
+
112
+ repl = AsyncREPL(action_registry=self.action_registry)
113
+
114
+ # Verify PromptSession was called with key_bindings
115
+ mock_session.assert_called_once()
116
+ call_kwargs = mock_session.call_args[1]
117
+ assert "key_bindings" in call_kwargs
118
+
119
+ key_bindings = call_kwargs["key_bindings"]
120
+ assert key_bindings is not None
121
+
122
+
123
+ class TestRunAsyncREPL:
124
+ """Test run_async_repl convenience function."""
125
+
126
+ def setup_method(self):
127
+ """Set up test fixtures."""
128
+ self.backend = MockBackend()
129
+
130
+ @pytest.mark.asyncio
131
+ async def test_run_async_repl_basic(self):
132
+ """Test basic run_async_repl functionality."""
133
+ with patch("repl_toolkit.async_repl.AsyncREPL") as mock_repl_class:
134
+ mock_repl = Mock()
135
+ mock_repl.run = AsyncMock()
136
+ mock_repl_class.return_value = mock_repl
137
+
138
+ await run_async_repl(backend=self.backend, initial_message="test message")
139
+
140
+ # Verify REPL was created and run with correct parameters
141
+ mock_repl_class.assert_called_once()
142
+ mock_repl.run.assert_called_once_with(self.backend, "test message")
143
+
144
+ @pytest.mark.asyncio
145
+ async def test_run_async_repl_with_registry(self):
146
+ """Test run_async_repl with custom action registry."""
147
+ custom_registry = ActionRegistry()
148
+
149
+ with patch("repl_toolkit.async_repl.AsyncREPL") as mock_repl_class:
150
+ mock_repl = Mock()
151
+ mock_repl.run = AsyncMock()
152
+ mock_repl_class.return_value = mock_repl
153
+
154
+ await run_async_repl(
155
+ backend=self.backend,
156
+ action_registry=custom_registry,
157
+ prompt_string="Custom: ",
158
+ history_path=Path("/tmp/custom_history.txt"),
159
+ )
160
+
161
+ # Verify parameters were passed correctly to AsyncREPL constructor
162
+ call_args = mock_repl_class.call_args[0]
163
+ call_kwargs = mock_repl_class.call_args[1] if mock_repl_class.call_args[1] else {}
164
+
165
+ # Check that action_registry was passed (could be positional or keyword)
166
+ assert (len(call_args) > 0 and call_args[0] is custom_registry) or call_kwargs.get(
167
+ "action_registry"
168
+ ) is custom_registry
169
+
170
+ # Verify run was called with backend and initial_message
171
+ mock_repl.run.assert_called_once_with(self.backend, None)
172
+
173
+
174
+ class TestREPLActionIntegration:
175
+ """Test integration between REPL and action system with late backend binding."""
176
+
177
+ def setup_method(self):
178
+ """Set up test fixtures."""
179
+ self.backend = MockBackend()
180
+ # Create action registry (backend will be injected later)
181
+ self.action_registry = ActionRegistry()
182
+ self.executed_actions = []
183
+
184
+ # Add test action
185
+ def test_handler(context):
186
+ self.executed_actions.append((context.triggered_by, context.args))
187
+
188
+ test_action = Action(
189
+ name="test_action",
190
+ description="Test action",
191
+ category="Test",
192
+ handler=test_handler,
193
+ command="/test",
194
+ keys="F9",
195
+ )
196
+ self.action_registry.register_action(test_action)
197
+
198
+ def test_action_registry_integration(self, mock_terminal_for_repl):
199
+ """Test that REPL properly integrates with action registry."""
200
+ repl = AsyncREPL(action_registry=self.action_registry)
201
+
202
+ # Registry should be the one we provided
203
+ assert repl.action_registry is self.action_registry
204
+
205
+ # Test action should be available
206
+ assert repl.action_registry.validate_action("test_action")
207
+ assert "/test" in repl.action_registry.command_map
208
+ assert "F9" in repl.action_registry.key_map
209
+
210
+ # Backend not set until run() is called
211
+ assert repl.action_registry.backend is None
212
+
213
+ def test_action_registry_without_backend(self, mock_terminal_for_repl):
214
+ """Test REPL with action registry that has no backend initially."""
215
+ registry_without_backend = ActionRegistry() # No backend
216
+ repl = AsyncREPL(action_registry=registry_without_backend)
217
+
218
+ # Registry should not have backend initially (as designed)
219
+ assert repl.action_registry.backend is None
220
+ assert repl.action_registry is registry_without_backend
221
+
222
+ def test_backend_injection_during_execution(self, mock_terminal_for_repl):
223
+ """Test that backend gets injected when needed."""
224
+ repl = AsyncREPL(action_registry=self.action_registry)
225
+
226
+ # Simulate what happens during run() - backend gets injected
227
+ repl.action_registry.backend = self.backend
228
+
229
+ # Now test command execution
230
+ repl.action_registry.handle_command("/test arg1 arg2")
231
+
232
+ # Verify action was executed
233
+ assert len(self.executed_actions) == 1
234
+ triggered_by, args = self.executed_actions[0]
235
+ assert triggered_by == "command"
236
+ assert args == ["arg1", "arg2"]
237
+
238
+ def test_shortcut_execution_integration(self, mock_terminal_for_repl):
239
+ """Test shortcut execution through REPL."""
240
+ repl = AsyncREPL(action_registry=self.action_registry)
241
+
242
+ # Inject backend (simulating what run() does)
243
+ repl.action_registry.backend = self.backend
244
+
245
+ # Simulate shortcut execution
246
+ mock_event = Mock()
247
+ repl.action_registry.handle_shortcut("F9", mock_event)
248
+
249
+ # Verify action was executed
250
+ assert len(self.executed_actions) == 1
251
+ triggered_by, _ = self.executed_actions[0]
252
+ assert triggered_by == "shortcut"
253
+
254
+
255
+ class TestErrorHandling:
256
+ """Test error handling in AsyncREPL with late backend binding."""
257
+
258
+ def setup_method(self):
259
+ """Set up test fixtures."""
260
+ self.backend = MockBackend()
261
+
262
+ def test_backend_error_handling(self, mock_terminal_for_repl):
263
+ """Test handling of backend errors."""
264
+ self.backend.should_succeed = False
265
+ repl = AsyncREPL()
266
+
267
+ # Should not raise error during initialization (no backend yet)
268
+ assert repl.action_registry is not None
269
+
270
+ def test_invalid_key_combination(self, mock_terminal_for_repl):
271
+ """Test handling of invalid key combinations."""
272
+ repl = AsyncREPL()
273
+
274
+ # Should handle invalid key combinations gracefully
275
+ result = repl._parse_key_combination("invalid-key-combo")
276
+ assert isinstance(result, tuple)
277
+
278
+ @patch("repl_toolkit.async_repl.logger")
279
+ def test_shortcut_registration_error(self, mock_logger):
280
+ """Test handling of shortcut registration errors."""
281
+ action_registry = ActionRegistry()
282
+
283
+ # Add action with potentially problematic key combo
284
+ action_registry.register_action(
285
+ Action(
286
+ name="problematic",
287
+ description="Problematic action",
288
+ category="Test",
289
+ handler=lambda ctx: None,
290
+ keys="invalid-combo",
291
+ )
292
+ )
293
+
294
+ # Should not raise error during REPL creation
295
+ repl = AsyncREPL(action_registry=action_registry)
296
+ assert repl is not None
297
+
298
+
299
+ class TestLateBackendBinding:
300
+ """Test late backend binding specific functionality."""
301
+
302
+ def setup_method(self):
303
+ """Set up test fixtures."""
304
+ self.backend = MockBackend()
305
+
306
+ def test_repl_creation_without_backend(self, mock_terminal_for_repl):
307
+ """Test that REPL can be created without backend."""
308
+ repl = AsyncREPL()
309
+
310
+ # Should create successfully
311
+ assert repl is not None
312
+ assert repl.action_registry is not None
313
+ assert repl.action_registry.backend is None
314
+
315
+ def test_backend_injection_pattern(self, mock_terminal_for_repl):
316
+ """Test the backend injection pattern."""
317
+ # Create REPL without backend
318
+ action_registry = ActionRegistry()
319
+ repl = AsyncREPL(action_registry=action_registry)
320
+
321
+ # Initially no backend
322
+ assert action_registry.backend is None
323
+
324
+ # Simulate backend becoming available (like in resource context)
325
+ action_registry.backend = self.backend
326
+
327
+ # Now backend is available
328
+ assert action_registry.backend is self.backend
329
+
330
+ def test_action_execution_with_late_backend(self, mock_terminal_for_repl):
331
+ """Test that actions work with late backend binding."""
332
+ executed = []
333
+
334
+ def test_handler(context):
335
+ # Handler can access backend through context
336
+ backend = context.backend
337
+ executed.append(backend is not None)
338
+
339
+ action_registry = ActionRegistry()
340
+ action_registry.register_action(
341
+ Action(
342
+ name="test",
343
+ description="Test",
344
+ category="Test",
345
+ handler=test_handler,
346
+ command="/test",
347
+ )
348
+ )
349
+
350
+ # Execute without backend
351
+ context = ActionContext(registry=action_registry, triggered_by="test")
352
+ action_registry.execute_action("test", context)
353
+ assert executed == [False] # No backend available
354
+
355
+ # Inject backend and execute again
356
+ executed.clear()
357
+ action_registry.backend = self.backend
358
+ context = ActionContext(registry=action_registry, backend=self.backend, triggered_by="test")
359
+ action_registry.execute_action("test", context)
360
+ assert executed == [True] # Backend available
361
+
362
+
363
+ class TestAsyncREPLEdgeCases:
364
+ """Test edge cases in AsyncREPL."""
365
+
366
+ def test_register_shortcuts_without_key_map(self, mock_terminal_for_repl):
367
+ """Test registering shortcuts when action_registry has no key_map."""
368
+
369
+ # Create a mock registry without key_map attribute
370
+ class MockRegistry:
371
+ pass
372
+
373
+ mock_registry = MockRegistry()
374
+ repl = AsyncREPL(action_registry=mock_registry)
375
+
376
+ # This should not raise - it should handle missing key_map gracefully
377
+ bindings = KeyBindings()
378
+ repl._register_action_shortcuts(bindings)
379
+
380
+ # Should have no bindings registered
381
+ assert len(bindings.bindings) == 0