1616@pytest .fixture
1717def restore_commands () -> Iterator [None ]:
1818 original_commands = special_main .COMMANDS .copy ()
19+ original_case_sensitive_commands = special_main .CASE_SENSITIVE_COMMANDS .copy ()
20+ original_case_insensitive_commands = special_main .CASE_INSENSITIVE_COMMANDS .copy ()
1921 try :
2022 yield
2123 finally :
2224 special_main .COMMANDS .clear ()
2325 special_main .COMMANDS .update (original_commands )
26+ special_main .CASE_SENSITIVE_COMMANDS .clear ()
27+ special_main .CASE_SENSITIVE_COMMANDS .update (original_case_sensitive_commands )
28+ special_main .CASE_INSENSITIVE_COMMANDS .clear ()
29+ special_main .CASE_INSENSITIVE_COMMANDS .update (original_case_insensitive_commands )
2430
2531
2632class FakeHelpCursor :
@@ -100,14 +106,35 @@ def handler() -> None:
100106 )
101107
102108
109+ def test_register_special_command_tracks_case_insensitive_commands (restore_commands : None ) -> None :
110+ special_main .COMMANDS .clear ()
111+ special_main .CASE_SENSITIVE_COMMANDS .clear ()
112+ special_main .CASE_INSENSITIVE_COMMANDS .clear ()
113+
114+ special_main .register_special_command (
115+ lambda : None ,
116+ 'Demo' ,
117+ 'demo' ,
118+ 'Description' ,
119+ aliases = ['\\ d' ],
120+ )
121+
122+ assert special_main .CASE_SENSITIVE_COMMANDS == set ()
123+ assert special_main .CASE_INSENSITIVE_COMMANDS == {'demo' , '\\ d' }
124+
125+
103126def test_special_command_decorator_registers_case_sensitive_command (restore_commands : None ) -> None :
104127 special_main .COMMANDS .clear ()
128+ special_main .CASE_SENSITIVE_COMMANDS .clear ()
129+ special_main .CASE_INSENSITIVE_COMMANDS .clear ()
105130
106131 @special_main .special_command ('Camel' , 'Camel' , 'Description' , case_sensitive = True )
107132 def handler () -> None :
108133 return None
109134
110135 assert special_main .COMMANDS ['Camel' ].handler is handler
136+ assert 'Camel' in special_main .CASE_SENSITIVE_COMMANDS
137+ assert special_main .CASE_INSENSITIVE_COMMANDS == set ()
111138 assert 'camel' not in special_main .COMMANDS
112139
113140
@@ -139,6 +166,26 @@ def test_execute_raises_for_case_sensitive_alias_lookup(restore_commands: None)
139166 special_main .execute (cast (Any , None ), 'DEMO' )
140167
141168
169+ def test_execute_raises_when_case_sensitive_exact_lookup_falls_back_to_lowercase (restore_commands : None ) -> None :
170+ special_main .COMMANDS .clear ()
171+ special_main .CASE_SENSITIVE_COMMANDS .clear ()
172+ special_main .CASE_INSENSITIVE_COMMANDS .clear ()
173+ special_main .COMMANDS ['camel' ] = special_main .SpecialCommand (
174+ lambda : None ,
175+ 'Camel' ,
176+ 'Camel' ,
177+ 'Description' ,
178+ arg_type = special_main .ArgType .NO_QUERY ,
179+ hidden = False ,
180+ case_sensitive = True ,
181+ shortcut = None ,
182+ )
183+ special_main .CASE_SENSITIVE_COMMANDS .add ('Camel' )
184+
185+ with pytest .raises (special_main .CommandNotFound , match = 'Command not found: Camel' ):
186+ special_main .execute (cast (Any , None ), 'Camel' )
187+
188+
142189def test_execute_dispatches_no_query_command (restore_commands : None ) -> None :
143190 calls : list [str ] = []
144191
@@ -236,8 +283,24 @@ def fake_show_keyword_help(cur: object, arg: str) -> list[SQLResult]:
236283 assert calls == [(cur , 'select' )]
237284
238285
286+ def test_execute_routes_uppercase_help_with_argument_to_keyword_help (monkeypatch ) -> None :
287+ calls : list [tuple [object , str ]] = []
288+
289+ def fake_show_keyword_help (cur : object , arg : str ) -> list [SQLResult ]:
290+ calls .append ((cur , arg ))
291+ return [SQLResult (status = 'keyword' )]
292+
293+ monkeypatch .setattr (special_main , 'show_keyword_help' , fake_show_keyword_help )
294+
295+ cur = object ()
296+ assert special_main .execute (cast (Any , cur ), 'HELP select' ) == [SQLResult (status = 'keyword' )]
297+ assert calls == [(cur , 'select' )]
298+
299+
239300def test_execute_raises_for_unknown_arg_type (restore_commands : None ) -> None :
240301 special_main .COMMANDS .clear ()
302+ special_main .CASE_SENSITIVE_COMMANDS .clear ()
303+ special_main .CASE_INSENSITIVE_COMMANDS .clear ()
241304 special_main .COMMANDS ['demo' ] = special_main .SpecialCommand (
242305 lambda : None ,
243306 'demo' ,
@@ -248,6 +311,7 @@ def test_execute_raises_for_unknown_arg_type(restore_commands: None) -> None:
248311 case_sensitive = False ,
249312 shortcut = None ,
250313 )
314+ special_main .CASE_INSENSITIVE_COMMANDS .add ('demo' )
251315
252316 with pytest .raises (special_main .CommandNotFound , match = 'Command type not found: demo' ):
253317 special_main .execute (cast (Any , None ), 'demo' )
@@ -265,6 +329,31 @@ def test_show_help_lists_only_visible_commands(restore_commands: None) -> None:
265329 assert result .postamble == f'Docs index — { DOCS_URL } '
266330
267331
332+ def test_show_keyword_help_for_special_command (restore_commands : None ) -> None :
333+ special_main .COMMANDS .clear ()
334+ special_main .CASE_SENSITIVE_COMMANDS .clear ()
335+ special_main .CASE_INSENSITIVE_COMMANDS .clear ()
336+ special_main .register_special_command (lambda : None , 'demo' , 'demo <arg>' , 'Demo command' )
337+
338+ result = special_main .show_keyword_help (cast (Any , None ), 'demo+' )[0 ]
339+
340+ assert result .header == ['name' , 'description' , 'example' ]
341+ assert result .rows == [('demo' , 'demo <arg>\n Demo command' , '' )]
342+
343+
344+ def test_show_keyword_help_for_case_sensitive_special_alias () -> None :
345+ result = special_main .show_keyword_help (cast (Any , None ), r'\e' )[0 ]
346+
347+ assert result .header == ['name' , 'description' , 'example' ]
348+ assert result .rows == [
349+ (
350+ r'\e' ,
351+ '<query>\\ edit | \\ edit <filename>\n Edit query with editor (uses $VISUAL or $EDITOR).' ,
352+ '' ,
353+ )
354+ ]
355+
356+
268357def test_show_keyword_help_exact_match () -> None :
269358 cur = FakeHelpCursor ([
270359 {'description' : [('name' , None )], 'rowcount' : 1 },
0 commit comments