From 1de1b662ae790b3ba3b575ee7d07acf6c1ea578e Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 28 Aug 2026 14:02:29 +0300 Subject: [PATCH] Render database names and failures verbatim in the inspector A database name, a location and an exception message all reach the info modal as configuration-derived text, and Rich reads `[...]` in any of them as markup: a name like `beta [prod]` disappeared, and a message carrying `[Errno 2]` or a stray closing tag could break the line it sat in. --- .../haiku/rag/inspector/widgets/info_modal.py | 26 +++++++----- tests/test_inspector.py | 40 ++++++++++++++++++- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py b/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py index 797286af..2a832108 100644 --- a/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py +++ b/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py @@ -1,6 +1,7 @@ import asyncio from typing import TYPE_CHECKING +from rich.markup import escape from textual.app import ComposeResult from textual.binding import Binding from textual.containers import Vertical, VerticalScroll @@ -35,12 +36,11 @@ async def database_lines(client: "HaikuRAG") -> list[str]: db = client.store.db stats = await get_database_stats(db) except Exception as e: - return [f"[red]Failed to open database: {e}[/red]"] + return [f"[red]Failed to open database: {escape(str(e))}[/red]"] - # The store read these on open; a second query would re-read and re-parse - # the same blob. + # The store read and parsed these on open. settings = client.store.stored_settings - stored_version = str(settings.get("version", "unknown")) + stored_version = escape(str(settings.get("version", "unknown"))) embed_model_obj = settings.get("embeddings", {}).get("model", {}) embed_provider = embed_model_obj.get("provider") embed_model = embed_model_obj.get("name") @@ -56,9 +56,9 @@ async def database_lines(client: "HaikuRAG") -> list[str]: ) if embed_provider or embed_model or vector_dim: - provider_part = embed_provider or "unknown" - model_part = embed_model or "unknown" - dim_part = f"{vector_dim}" if vector_dim is not None else "unknown" + provider_part = escape(str(embed_provider or "unknown")) + model_part = escape(str(embed_model or "unknown")) + dim_part = escape(str(vector_dim)) if vector_dim is not None else "unknown" lines.append( f"[bold $accent]embeddings[/bold $accent]: " f"{provider_part}/{model_part} (dim: {dim_part})" @@ -172,11 +172,15 @@ class InfoModal(ModalScreen): for block in blocks: lines.extend(block) else: - lines.append(f"[bold $accent]location[/bold $accent]: {location}") + lines.append( + f"[bold $accent]location[/bold $accent]: {escape(str(location))}" + ) lines.extend(await database_lines(self.client)) lines.append("[bold]Versions[/bold]") - versions = get_package_versions() + versions = { + key: escape(str(value)) for key, value in get_package_versions().items() + } lines.append(f"[bold $accent]haiku.rag[/bold $accent]: {versions['haiku_rag']}") lines.append(f"[bold $accent]lancedb[/bold $accent]: {versions['lancedb']}") lines.append(f"[bold $accent]docling[/bold $accent]: {versions['docling']}") @@ -192,13 +196,13 @@ class InfoModal(ModalScreen): async def _report(self, name: str) -> list[str]: """One database's block, including its own failure to open.""" - lines = [f"[bold]{name}[/bold]"] + lines = [f"[bold]{escape(name)}[/bold]"] try: (owner,) = await self.client.clients_for([name]) except Exception as e: # The client names a configured database by name and never by # location, so its message is safe to show. - return [*lines, f"[red]{e}[/red]", ""] + return [*lines, f"[red]{escape(str(e))}[/red]", ""] return [*lines, *await database_lines(owner)] async def action_dismiss(self, result=None) -> None: diff --git a/tests/test_inspector.py b/tests/test_inspector.py index 144f6a65..47e6cfb3 100644 --- a/tests/test_inspector.py +++ b/tests/test_inspector.py @@ -446,9 +446,47 @@ class TestReportingEachDatabase: "database 'beta' could not be opened: OSError" ) modal.client = client - modal.db_path = None lines = await modal._report("beta") assert lines[0] == "[bold]beta[/bold]" assert "could not be opened" in lines[1] + + @pytest.mark.asyncio + async def test_a_failure_with_bracketed_text_renders_literally(self): + """Error text and database names render verbatim, not as Rich markup.""" + from rich.text import Text + + from haiku.rag.inspector.widgets.info_modal import InfoModal + from haiku.rag.store.exceptions import SourceUnavailableError + + modal = InfoModal.__new__(InfoModal) + client = AsyncMock() + message = "database 'beta [prod]' could not be opened: [/red] [Errno 2]" + client.clients_for.side_effect = SourceUnavailableError(message) + modal.client = client + + lines = await modal._report("beta [prod]") + + assert Text.from_markup(lines[0]).plain == "beta [prod]" + assert message in Text.from_markup(lines[1]).plain + + @pytest.mark.asyncio + async def test_an_open_failure_with_bracketed_text_renders_literally( + self, tmp_path + ): + """A stats-read failure renders its message verbatim, not as markup.""" + from rich.text import Text + + from haiku.rag.inspector.widgets.info_modal import database_lines + from haiku.rag.store.engine import ConnectionMode + + client = MagicMock() + client.store.db_path = tmp_path + client.store._connection_mode = ConnectionMode.LOCAL + message = "Schema error: no field named [vector, text] [/red]" + type(client.store).db = property(MagicMock(side_effect=RuntimeError(message))) + + lines = await database_lines(client) + + assert message in Text.from_markup(lines[0]).plain