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,578 @@
1
+ """
2
+ Tests for the action system.
3
+ """
4
+
5
+ from unittest.mock import Mock
6
+
7
+ import pytest
8
+
9
+ from repl_toolkit.actions import (
10
+ Action,
11
+ ActionContext,
12
+ ActionError,
13
+ ActionExecutionError,
14
+ ActionRegistry,
15
+ ActionValidationError,
16
+ )
17
+
18
+
19
+ class TestAction:
20
+ """Test Action dataclass functionality."""
21
+
22
+ def test_action_creation_minimal(self):
23
+ """Test creating action with minimal required parameters."""
24
+ action = Action(
25
+ name="test_action",
26
+ description="Test action",
27
+ category="Test",
28
+ handler=lambda ctx: None,
29
+ command="/test",
30
+ )
31
+
32
+ assert action.name == "test_action"
33
+ assert action.description == "Test action"
34
+ assert action.category == "Test"
35
+ assert action.command == "/test"
36
+ assert action.has_command
37
+ assert not action.has_shortcut
38
+
39
+ def test_action_creation_full(self):
40
+ """Test creating action with all parameters."""
41
+ handler = lambda ctx: None
42
+
43
+ action = Action(
44
+ name="full_action",
45
+ description="Full test action",
46
+ category="Test",
47
+ handler=handler,
48
+ command="/full",
49
+ command_usage="/full [args] - Full test command",
50
+ keys="F2",
51
+ keys_description="Full test shortcut",
52
+ enabled=True,
53
+ context="test_context",
54
+ )
55
+
56
+ assert action.name == "full_action"
57
+ assert action.has_command
58
+ assert action.has_shortcut
59
+ assert action.enabled
60
+ assert action.context == "test_context"
61
+
62
+ def test_action_validation_errors(self):
63
+ """Test action validation failures."""
64
+ # Empty name
65
+ with pytest.raises(ValueError, match="Action name cannot be empty"):
66
+ Action(
67
+ name="",
68
+ description="Test",
69
+ category="Test",
70
+ handler=lambda ctx: None,
71
+ command="/test",
72
+ )
73
+
74
+ # Empty description
75
+ with pytest.raises(ValueError, match="Action description cannot be empty"):
76
+ Action(
77
+ name="test",
78
+ description="",
79
+ category="Test",
80
+ handler=lambda ctx: None,
81
+ command="/test",
82
+ )
83
+
84
+ # No command or keys
85
+ with pytest.raises(ValueError, match="Action must have either command or keys binding"):
86
+ Action(name="test", description="Test", category="Test", handler=lambda ctx: None)
87
+
88
+ # Invalid command format
89
+ with pytest.raises(ValueError, match="Command 'test' must start with '/'"):
90
+ Action(
91
+ name="test",
92
+ description="Test",
93
+ category="Test",
94
+ handler=lambda ctx: None,
95
+ command="test",
96
+ )
97
+
98
+ def test_keys_list_handling(self):
99
+ """Test handling of keys as string vs list."""
100
+ # Single key as string
101
+ action1 = Action(
102
+ name="test1", description="Test", category="Test", handler=lambda ctx: None, keys="F1"
103
+ )
104
+ assert action1.get_keys_list() == ["F1"]
105
+
106
+ # Multiple keys as list
107
+ action2 = Action(
108
+ name="test2",
109
+ description="Test",
110
+ category="Test",
111
+ handler=lambda ctx: None,
112
+ keys=["F1", "ctrl-h"],
113
+ )
114
+ assert action2.get_keys_list() == ["F1", "ctrl-h"]
115
+
116
+ # No keys
117
+ action3 = Action(
118
+ name="test3",
119
+ description="Test",
120
+ category="Test",
121
+ handler=lambda ctx: None,
122
+ command="/test",
123
+ )
124
+ assert action3.get_keys_list() == []
125
+
126
+
127
+ class TestActionContext:
128
+ """Test ActionContext functionality."""
129
+
130
+ def test_context_creation(self):
131
+ """Test action context creation."""
132
+ registry = Mock()
133
+ backend = Mock()
134
+
135
+ context = ActionContext(
136
+ registry=registry, backend=backend, args=["arg1", "arg2"], triggered_by="command"
137
+ )
138
+
139
+ assert context.registry is registry
140
+ assert context.backend is backend
141
+ assert context.args == ["arg1", "arg2"]
142
+ assert context.triggered_by == "command"
143
+
144
+ def test_context_triggered_by_detection(self):
145
+ """Test automatic triggered_by detection."""
146
+ registry = Mock()
147
+
148
+ # Should detect shortcut from event
149
+ context1 = ActionContext(registry=registry, event=Mock())
150
+ assert context1.triggered_by == "shortcut"
151
+
152
+ # Should detect command from args
153
+ context2 = ActionContext(registry=registry, args=["arg"])
154
+ assert context2.triggered_by == "command"
155
+
156
+ # Should detect command from user_input
157
+ context3 = ActionContext(registry=registry, user_input="/test")
158
+ assert context3.triggered_by == "command"
159
+
160
+ # Should default to programmatic
161
+ context4 = ActionContext(registry=registry)
162
+ assert context4.triggered_by == "programmatic"
163
+
164
+ def test_context_printer_default(self):
165
+ """Test that printer defaults to print."""
166
+ registry = Mock()
167
+ context = ActionContext(registry=registry)
168
+ assert context.printer == print
169
+
170
+ def test_context_custom_printer(self):
171
+ """Test that custom printer can be set."""
172
+ registry = Mock()
173
+ custom_printer = Mock()
174
+ context = ActionContext(registry=registry, printer=custom_printer)
175
+ assert context.printer == custom_printer
176
+
177
+
178
+ class TestActionRegistry:
179
+ """Test ActionRegistry functionality."""
180
+
181
+ def setup_method(self):
182
+ """Set up test registry."""
183
+ self.registry = ActionRegistry()
184
+
185
+ def test_registry_initialization(self):
186
+ """Test registry initializes with built-in actions."""
187
+ assert len(self.registry.actions) > 0
188
+ assert "show_help" in self.registry.actions
189
+ assert "/help" in self.registry.command_map
190
+ assert "F1" in self.registry.key_map
191
+
192
+ def test_registry_custom_printer(self):
193
+ """Test registry with custom printer."""
194
+ mock_printer = Mock()
195
+ registry = ActionRegistry(printer=mock_printer)
196
+ assert registry.printer == mock_printer
197
+
198
+ def test_registry_default_printer(self):
199
+ """Test registry defaults to print."""
200
+ registry = ActionRegistry()
201
+ assert registry.printer == print
202
+
203
+ def test_register_action(self):
204
+ """Test action registration."""
205
+ action = Action(
206
+ name="test_action",
207
+ description="Test action",
208
+ category="Test",
209
+ handler=lambda ctx: None,
210
+ command="/test",
211
+ keys="F10",
212
+ )
213
+
214
+ self.registry.register_action(action)
215
+
216
+ assert "test_action" in self.registry.actions
217
+ assert "/test" in self.registry.command_map
218
+ assert "F10" in self.registry.key_map
219
+ assert self.registry.command_map["/test"] == "test_action"
220
+ assert self.registry.key_map["F10"] == "test_action"
221
+
222
+ def test_register_action_conflicts(self):
223
+ """Test action registration conflict detection."""
224
+ action1 = Action(
225
+ name="action1",
226
+ description="Test",
227
+ category="Test",
228
+ handler=lambda ctx: None,
229
+ command="/test",
230
+ )
231
+ action2 = Action(
232
+ name="action2",
233
+ description="Test",
234
+ category="Test",
235
+ handler=lambda ctx: None,
236
+ command="/test", # Same command
237
+ )
238
+
239
+ self.registry.register_action(action1)
240
+
241
+ with pytest.raises(ActionValidationError, match="Command '/test' already bound"):
242
+ self.registry.register_action(action2)
243
+
244
+ def test_convenience_registration_methods(self):
245
+ """Test convenience registration methods."""
246
+ # Test action registration with both command and keys
247
+ self.registry.register_action(
248
+ name="both_test",
249
+ description="Both test",
250
+ category="Test",
251
+ handler=lambda ctx: None,
252
+ command="/both",
253
+ keys="F11",
254
+ )
255
+
256
+ assert "both_test" in self.registry.actions
257
+ assert "/both" in self.registry.command_map
258
+ assert "F11" in self.registry.key_map
259
+
260
+ # Test command-only registration
261
+ self.registry.register_action(
262
+ name="cmd_test",
263
+ command="/cmdonly",
264
+ description="Command only",
265
+ category="Test",
266
+ handler=lambda ctx: None,
267
+ )
268
+
269
+ action = self.registry.get_action("cmd_test")
270
+ assert action.has_command
271
+ assert not action.has_shortcut
272
+
273
+ # Test shortcut-only registration
274
+ self.registry.register_action(
275
+ name="key_test",
276
+ keys="F12",
277
+ description="Key only",
278
+ category="Test",
279
+ handler=lambda ctx: None,
280
+ )
281
+
282
+ action = self.registry.get_action("key_test")
283
+ assert not action.has_command
284
+ assert action.has_shortcut
285
+
286
+ def test_action_lookup_methods(self):
287
+ """Test action lookup methods."""
288
+ action = Action(
289
+ name="lookup_test",
290
+ description="Lookup test",
291
+ category="Test",
292
+ handler=lambda ctx: None,
293
+ command="/lookup",
294
+ keys="ctrl-l",
295
+ )
296
+
297
+ self.registry.register_action(action)
298
+
299
+ # Test lookup by name
300
+ found = self.registry.get_action("lookup_test")
301
+ assert found is action
302
+
303
+ # Test lookup by command
304
+ found = self.registry.get_action_by_command("/lookup")
305
+ assert found is action
306
+
307
+ # Test lookup by keys
308
+ found = self.registry.get_action_by_keys("ctrl-l")
309
+ assert found is action
310
+
311
+ # Test not found cases
312
+ assert self.registry.get_action("nonexistent") is None
313
+ assert self.registry.get_action_by_command("/nonexistent") is None
314
+ assert self.registry.get_action_by_keys("nonexistent") is None
315
+
316
+ def test_execute_action(self):
317
+ """Test action execution."""
318
+ executed = []
319
+
320
+ def test_handler(context):
321
+ executed.append(context.triggered_by)
322
+
323
+ action = Action(
324
+ name="exec_test",
325
+ description="Execution test",
326
+ category="Test",
327
+ handler=test_handler,
328
+ command="/exec",
329
+ )
330
+
331
+ self.registry.register_action(action)
332
+
333
+ context = ActionContext(registry=self.registry, triggered_by="test")
334
+
335
+ self.registry.execute_action("exec_test", context)
336
+ assert executed == ["test"]
337
+
338
+ def test_execute_nonexistent_action(self):
339
+ """Test executing nonexistent action."""
340
+ context = ActionContext(registry=self.registry)
341
+
342
+ with pytest.raises(ActionError, match="Action 'nonexistent' not found"):
343
+ self.registry.execute_action("nonexistent", context)
344
+
345
+ def test_execute_disabled_action(self):
346
+ """Test executing disabled action."""
347
+ action = Action(
348
+ name="disabled_test",
349
+ description="Disabled test",
350
+ category="Test",
351
+ handler=lambda ctx: None,
352
+ command="/disabled",
353
+ enabled=False,
354
+ )
355
+
356
+ self.registry.register_action(action)
357
+ context = ActionContext(registry=self.registry)
358
+
359
+ # Should not raise error, but should not execute
360
+ self.registry.execute_action("disabled_test", context)
361
+
362
+ def test_handle_command(self):
363
+ """Test command handling."""
364
+ executed = []
365
+
366
+ def test_handler(context):
367
+ executed.append(context.args)
368
+
369
+ action = Action(
370
+ name="cmd_test",
371
+ description="Command test",
372
+ category="Test",
373
+ handler=test_handler,
374
+ command="/cmdtest",
375
+ )
376
+
377
+ self.registry.register_action(action)
378
+
379
+ self.registry.handle_command("/cmdtest arg1 arg2")
380
+ assert executed == [["arg1", "arg2"]]
381
+
382
+ def test_handle_command_with_custom_printer(self):
383
+ """Test command handling with custom printer."""
384
+ mock_printer = Mock()
385
+ registry = ActionRegistry(printer=mock_printer)
386
+
387
+ # Test unknown command uses custom printer
388
+ registry.handle_command("/unknown")
389
+
390
+ # Should have printed to custom printer
391
+ assert mock_printer.call_count >= 1
392
+ calls = [str(call) for call in mock_printer.call_args_list]
393
+ assert any("Unknown command" in str(call) for call in calls)
394
+
395
+ def test_handle_unknown_command(self):
396
+ """Test handling unknown command."""
397
+ # Should not raise error, just print message
398
+ self.registry.handle_command("/unknown")
399
+
400
+ def test_handle_shortcut(self):
401
+ """Test shortcut handling."""
402
+ executed = []
403
+
404
+ def test_handler(context):
405
+ executed.append(context.event)
406
+
407
+ action = Action(
408
+ name="key_test",
409
+ description="Key test",
410
+ category="Test",
411
+ handler=test_handler,
412
+ keys="F5",
413
+ )
414
+
415
+ self.registry.register_action(action)
416
+
417
+ mock_event = Mock()
418
+ self.registry.handle_shortcut("F5", mock_event)
419
+ assert executed == [mock_event]
420
+
421
+ def test_handle_unknown_shortcut(self):
422
+ """Test handling unknown shortcut."""
423
+ # Should not raise error, just log
424
+ self.registry.handle_shortcut("unknown", Mock())
425
+
426
+ def test_printer_propagation_to_context(self):
427
+ """Test that printer is propagated to ActionContext."""
428
+ mock_printer = Mock()
429
+ registry = ActionRegistry(printer=mock_printer)
430
+
431
+ outputs = []
432
+
433
+ def test_handler(context):
434
+ # Verify context has the custom printer
435
+ assert context.printer == mock_printer
436
+ context.printer("Test output")
437
+ outputs.append("executed")
438
+
439
+ action = Action(
440
+ name="printer_test",
441
+ description="Printer test",
442
+ category="Test",
443
+ handler=test_handler,
444
+ command="/printertest",
445
+ )
446
+
447
+ registry.register_action(action)
448
+ registry.handle_command("/printertest")
449
+
450
+ # Verify handler was executed
451
+ assert outputs == ["executed"]
452
+
453
+ # Verify printer was called
454
+ mock_printer.assert_called_with("Test output")
455
+
456
+ def test_list_methods(self):
457
+ """Test list methods."""
458
+ actions = self.registry.list_actions()
459
+ assert isinstance(actions, list)
460
+ assert "show_help" in actions
461
+
462
+ commands = self.registry.list_commands()
463
+ assert isinstance(commands, list)
464
+ assert "/help" in commands
465
+
466
+ shortcuts = self.registry.list_shortcuts()
467
+ assert isinstance(shortcuts, list)
468
+ assert "F1" in shortcuts
469
+
470
+ def test_categories(self):
471
+ """Test category organization."""
472
+ categories = self.registry.get_actions_by_category()
473
+ assert isinstance(categories, dict)
474
+ assert "General" in categories
475
+ assert len(categories["General"]) > 0
476
+
477
+ def test_builtin_help_action(self):
478
+ """Test built-in help action."""
479
+ # Test general help
480
+ context = ActionContext(registry=self.registry, args=[])
481
+ self.registry.execute_action("show_help", context)
482
+
483
+ # Test specific help
484
+ context = ActionContext(registry=self.registry, args=["show_help"])
485
+ self.registry.execute_action("show_help", context)
486
+
487
+ # Test help for nonexistent action
488
+ context = ActionContext(registry=self.registry, args=["nonexistent"])
489
+ self.registry.execute_action("show_help", context)
490
+
491
+ def test_builtin_help_action_custom_printer(self):
492
+ """Test built-in help action with custom printer."""
493
+ mock_printer = Mock()
494
+ registry = ActionRegistry(printer=mock_printer)
495
+
496
+ # Execute help command
497
+ registry.handle_command("/help")
498
+
499
+ # Verify custom printer was called
500
+ assert mock_printer.call_count > 0
501
+
502
+ # Check that help output was sent to custom printer
503
+ calls = [str(call) for call in mock_printer.call_args_list]
504
+ assert any("Available Actions" in str(call) for call in calls)
505
+
506
+ def test_builtin_shortcuts_action(self):
507
+ """Test built-in shortcuts listing action."""
508
+ context = ActionContext(registry=self.registry, args=[])
509
+ self.registry.execute_action("list_shortcuts", context)
510
+
511
+ def test_builtin_shortcuts_action_custom_printer(self):
512
+ """Test built-in shortcuts action with custom printer."""
513
+ mock_printer = Mock()
514
+ registry = ActionRegistry(printer=mock_printer)
515
+
516
+ # Execute shortcuts command
517
+ registry.handle_command("/shortcuts")
518
+
519
+ # Verify custom printer was called
520
+ assert mock_printer.call_count > 0
521
+
522
+ # Check that shortcuts output was sent to custom printer
523
+ calls = [str(call) for call in mock_printer.call_args_list]
524
+ assert any("Keyboard Shortcuts" in str(call) for call in calls)
525
+
526
+
527
+ class TestActionHandlerProtocol:
528
+ """Test ActionHandler protocol compliance."""
529
+
530
+ def test_protocol_compliance(self):
531
+ """Test that ActionRegistry implements ActionHandler protocol."""
532
+ from repl_toolkit.ptypes import ActionHandler
533
+
534
+ registry = ActionRegistry()
535
+ assert isinstance(registry, ActionHandler)
536
+
537
+ # Test protocol methods
538
+ assert hasattr(registry, "execute_action")
539
+ assert hasattr(registry, "handle_command")
540
+ assert hasattr(registry, "validate_action")
541
+ assert hasattr(registry, "list_actions")
542
+
543
+ def test_validate_action(self):
544
+ """Test action validation."""
545
+ registry = ActionRegistry()
546
+
547
+ assert registry.validate_action("show_help") # Built-in action
548
+ assert not registry.validate_action("nonexistent")
549
+
550
+ def test_list_actions(self):
551
+ """Test action listing."""
552
+ registry = ActionRegistry()
553
+ actions = registry.list_actions()
554
+
555
+ assert isinstance(actions, list)
556
+ assert len(actions) > 0
557
+ assert "show_help" in actions
558
+
559
+
560
+ class TestActionValidationExtended:
561
+ """Additional validation tests for Action."""
562
+
563
+ def test_empty_category_validation(self):
564
+ """Test that empty category raises ValueError."""
565
+ with pytest.raises(ValueError, match="category cannot be empty"):
566
+ Action(
567
+ name="test",
568
+ description="Test",
569
+ category="", # Empty category
570
+ handler=lambda ctx: None,
571
+ )
572
+
573
+ def test_empty_string_handler_validation(self):
574
+ """Test that empty string handler raises ValueError."""
575
+ with pytest.raises(ValueError, match="handler cannot be empty string"):
576
+ Action(
577
+ name="test", description="Test", category="Test", handler="" # Empty string handler
578
+ )