@@ -296,6 +296,186 @@ def save_query(self, connection_name, query):
296296 assert all (entry .connection_name == "saved-db" for entry in screen ._merged_entries )
297297
298298
299+ class TestQueryHistoryStarringDuplicates :
300+ """Regression tests for starring behavior when multiple history entries
301+ share the same query text (e.g., same SQL run against multiple databases
302+ on one connection — file-backed dedup is per-(connection, database))."""
303+
304+ def _make_screen (
305+ self ,
306+ history : list [QueryHistoryEntry ],
307+ starred : set [str ],
308+ connection_name : str = "saved-db" ,
309+ ) -> QueryHistoryScreen :
310+ return QueryHistoryScreen (
311+ history = history ,
312+ connection_name = connection_name ,
313+ starred = starred ,
314+ )
315+
316+ def test_same_query_across_databases_collapses_to_single_row (self ) -> None :
317+ history = [
318+ QueryHistoryEntry (
319+ query = "SELECT 1" ,
320+ timestamp = "2026-01-02T00:00:00" ,
321+ connection_name = "saved-db" ,
322+ database = "db_a" ,
323+ ),
324+ QueryHistoryEntry (
325+ query = "SELECT 1" ,
326+ timestamp = "2026-01-01T00:00:00" ,
327+ connection_name = "saved-db" ,
328+ database = "db_b" ,
329+ ),
330+ ]
331+ screen = self ._make_screen (history , starred = {"SELECT 1" })
332+
333+ merged = screen ._merge_entries ()
334+
335+ assert len (merged ) == 1 , (
336+ "Same query text from two databases should collapse to one row "
337+ "in the history display (starring is keyed by text, so the row "
338+ "count must match)."
339+ )
340+ assert merged [0 ].is_starred
341+ # Most-recent occurrence wins the row.
342+ assert merged [0 ].database == "db_a"
343+
344+ @pytest .mark .asyncio
345+ async def test_starring_one_marks_duplicate_in_other_database (self ) -> None :
346+ saved_conn = create_test_connection ("saved-db" , "sqlite" )
347+ entries = [
348+ QueryHistoryEntry (
349+ query = "SELECT 1" ,
350+ timestamp = "2026-01-02T00:00:00" ,
351+ connection_name = "saved-db" ,
352+ database = "db_a" ,
353+ ),
354+ QueryHistoryEntry (
355+ query = "SELECT 1" ,
356+ timestamp = "2026-01-01T00:00:00" ,
357+ connection_name = "saved-db" ,
358+ database = "db_b" ,
359+ ),
360+ ]
361+
362+ class StubHistoryStore :
363+ def __init__ (self , entries ):
364+ self ._entries = entries
365+
366+ def load_all (self ):
367+ return list (self ._entries )
368+
369+ def load_for_connection (self , connection_name ):
370+ return [e for e in self ._entries if e .connection_name == connection_name ]
371+
372+ def delete_entry (self , connection_name , timestamp ):
373+ _ = connection_name
374+ _ = timestamp
375+ return False
376+
377+ def save_query (self , connection_name , query , database = "" ):
378+ _ = connection_name
379+ _ = query
380+ _ = database
381+
382+ history_store = StubHistoryStore (entries )
383+ services = build_test_services (
384+ connection_store = MockConnectionStore ([saved_conn ]),
385+ settings_store = MockSettingsStore ({"theme" : "tokyo-night" }),
386+ history_store = history_store ,
387+ )
388+ app = SSMSTUI (services = services )
389+
390+ async with app .run_test (size = (100 , 35 )) as pilot :
391+ app .connections = [saved_conn ]
392+ app .current_config = saved_conn
393+
394+ # Open history, star the first row (which corresponds to db_a),
395+ # close — emulates: user opens history, presses '*' on one row.
396+ app .action_show_history ()
397+ await pilot .pause (0.2 )
398+ screen = next (
399+ (s for s in app .screen_stack if isinstance (s , QueryHistoryScreen )),
400+ None ,
401+ )
402+ assert screen is not None
403+ option_list = screen .query_one ("#history-list" , OptionList )
404+ # The two same-text rows should already be collapsed to one.
405+ assert option_list .option_count == 1
406+ assert option_list .highlighted == 0
407+ screen .action_toggle_star ()
408+ await pilot .pause (0.2 )
409+
410+ # Re-open: exactly one starred row, not two.
411+ app .action_show_history ()
412+ await pilot .pause (0.2 )
413+ screen = next (
414+ (s for s in app .screen_stack if isinstance (s , QueryHistoryScreen )),
415+ None ,
416+ )
417+ assert screen is not None
418+ starred_entries = [e for e in screen ._merged_entries if e .is_starred ]
419+ try :
420+ assert len (starred_entries ) == 1 , (
421+ f"Expected exactly one starred row after toggling one; "
422+ f"got { len (starred_entries )} "
423+ )
424+ finally :
425+ # The shared test config dir is module-scoped (see
426+ # tests/fixtures/utils.py), so any star we save here would
427+ # leak to later tests. Clean up.
428+ services .starred_store .clear_for_connection (saved_conn .name )
429+
430+
431+ class TestQueryHistoryBracketIdentifierRendering :
432+ """Regression tests for MSSQL bracket identifiers like [dbo].[ServiceFeatures]
433+ being silently eaten by Textual markup when the bracket contents start with
434+ an uppercase letter (rich.markup.escape only escapes [a-z#/@] tags)."""
435+
436+ @pytest .mark .asyncio
437+ async def test_bracket_identifier_with_uppercase_not_truncated (self ) -> None :
438+ from textual .app import App
439+ from textual .widgets .option_list import Option
440+
441+ # Drive the option directly through a minimal Textual app so we
442+ # observe what _build_option's text actually renders to.
443+ screen = QueryHistoryScreen (
444+ history = [
445+ QueryHistoryEntry (
446+ query = "SELECT TOP 100 * FROM [dbo].[ServiceFeatures]" ,
447+ timestamp = "2026-01-01T00:00:00" ,
448+ connection_name = "saved-db" ,
449+ ),
450+ ],
451+ connection_name = "saved-db" ,
452+ starred = set (),
453+ )
454+ screen ._merged_entries = screen ._merge_entries ()
455+ built = screen ._build_option (screen ._merged_entries [0 ])
456+
457+ class _ProbeApp (App ):
458+ def compose (self ):
459+ yield OptionList (Option (built .prompt , id = built .id or "probe" ), id = "probe-list" )
460+
461+ app = _ProbeApp ()
462+ async with app .run_test (size = (120 , 10 )) as pilot :
463+ await pilot .pause ()
464+ option_list = app .query_one ("#probe-list" , OptionList )
465+ lines = option_list .render_lines (option_list .region )
466+ rendered_text = "\n " .join (
467+ "" .join (seg .text for seg in line ) for line in lines
468+ )
469+
470+ assert "[ServiceFeatures]" in rendered_text , (
471+ "Bracket identifier with uppercase first letter was silently "
472+ "swallowed by Textual markup (rich.markup.escape only escapes "
473+ "tags starting with [a-z#/@], so '[ServiceFeatures]' is left "
474+ "unescaped and the markup parser eats it). Visible text:\n "
475+ + rendered_text
476+ )
477+
478+
299479class TestQueryHistoryVimNavigation :
300480 """Tests for j/k vim-style navigation in the history screen."""
301481
0 commit comments