From 93824cdee58fc8a851bf943c84d45e94242873b8 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Sat, 25 Jul 2026 11:27:29 +0300 Subject: [PATCH] Surface database-open errors in the TUIs ChatApp and InspectorApp assigned self.client before __aenter__ completed, so a failed open was masked by an AttributeError from on_unmount tearing down the never-opened client. --- CHANGELOG.md | 4 ++++ haiku_rag_slim/haiku/rag/chat/app.py | 7 +++++-- haiku_rag_slim/haiku/rag/inspector/app.py | 7 +++++-- tests/chat/test_chat_app.py | 12 ++++++++++++ tests/test_inspector.py | 12 ++++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a59f7f7e..0d45a598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - MCP `ask_question` and `analyze` tools accept `images_base64`. - Chat TUI: `Ctrl+I` opens an image picker; attached images insert `[Image #N]` tokens in a multi-line prompt and are sent to the model with the message. +### Fixed + +- Chat and inspector TUIs report the actual database-open error instead of an `AttributeError` from teardown. + ## [0.69.0] - 2026-07-24 ### Added diff --git a/haiku_rag_slim/haiku/rag/chat/app.py b/haiku_rag_slim/haiku/rag/chat/app.py index 53014e3d..0350e5d4 100644 --- a/haiku_rag_slim/haiku/rag/chat/app.py +++ b/haiku_rag_slim/haiku/rag/chat/app.py @@ -139,12 +139,15 @@ class ChatApp(App): async def on_mount(self) -> None: """Initialize the app when mounted.""" - self.client = HaikuRAG( + client = HaikuRAG( db_path=self.db_path, config=self.config, read_only=self.read_only, ) - await self.client.__aenter__() + # Assign only after a successful open: on_unmount must not tear down + # a client whose __aenter__ failed. + await client.__aenter__() + self.client = client self._agent = Agent( self._model, diff --git a/haiku_rag_slim/haiku/rag/inspector/app.py b/haiku_rag_slim/haiku/rag/inspector/app.py index 369e4f45..d94b035e 100644 --- a/haiku_rag_slim/haiku/rag/inspector/app.py +++ b/haiku_rag_slim/haiku/rag/inspector/app.py @@ -83,12 +83,15 @@ class InspectorApp(App): async def on_mount(self) -> None: """Initialize the app when mounted.""" config = get_config() - self.client = HaikuRAG( + client = HaikuRAG( db_path=self.db_path, config=config, read_only=self.read_only, ) - await self.client.__aenter__() + # Assign only after a successful open: on_unmount must not tear down + # a client whose __aenter__ failed. + await client.__aenter__() + self.client = client # Load initial documents doc_list = self.query_one(DocumentList) diff --git a/tests/chat/test_chat_app.py b/tests/chat/test_chat_app.py index 94da13ef..40057501 100644 --- a/tests/chat/test_chat_app.py +++ b/tests/chat/test_chat_app.py @@ -400,3 +400,15 @@ async def test_document_filter_cleared_when_empty(temp_db_path: Path): rag_state = RAGState.model_validate(app._state[RAG_STATE_NAMESPACE]) assert rag_state.document_filter is None assert app._state["rag"]["document_filter"] is None + + +@pytest.mark.asyncio +async def test_chat_app_open_failure_surfaces_real_error(tmp_path: Path): + """A failed database open must surface its own error, not an + AttributeError from tearing down a client that never opened.""" + from haiku.rag.chat.app import ChatApp + + app = ChatApp(db_path=tmp_path / "missing.lancedb", capabilities=[]) + with pytest.raises(FileNotFoundError): + async with app.run_test(): + pass diff --git a/tests/test_inspector.py b/tests/test_inspector.py index 8c3f6ee8..9ee6d476 100644 --- a/tests/test_inspector.py +++ b/tests/test_inspector.py @@ -223,3 +223,15 @@ async def test_context_modal_suppresses_pictures_when_vision_disabled(): await pilot.pause() modal = app.screen assert list(modal.query(TextualImage)) == [] + + +@pytest.mark.asyncio +async def test_inspector_open_failure_surfaces_real_error(tmp_path): + """A failed database open must surface its own error, not an + AttributeError from tearing down a client that never opened.""" + from haiku.rag.inspector.app import InspectorApp + + app = InspectorApp(db_path=tmp_path / "missing.lancedb", read_only=True) + with pytest.raises(FileNotFoundError): + async with app.run_test(): + pass