From d93cb5c891faa6df3976fcd2911210ec1de5418c Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 27 Aug 2026 14:49:40 +0300 Subject: [PATCH] Refuse a name the client does not cover, however many it covers `reader_for` returned itself before looking at the name, so an "alpha" client answered `reader_for("beta")` with alpha's reader. A citation naming another database would have been read from the wrong one. Both paths decide through `clients_covering` now, so one database refuses a wrong name the way a set already did. The chat citation test said mounting lends the client without showing it; a chat test shows it and the docstring points there. --- haiku_rag_slim/haiku/rag/client/__init__.py | 13 ++++++------- tests/chat/test_chat_app.py | 19 +++++++++++++++++++ tests/multi_db/test_capabilities.py | 6 +++--- tests/multi_db/test_scope.py | 21 ++++++++++++++++++++- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index d6131e05..bf5c0092 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -198,15 +198,14 @@ class HaikuRAG: database. None only when a client covering a set is given no name, as for evidence - recorded before databases could be named. A name outside the set raises - `KeyError`, the same as `clients_for`: provenance naming a database this - client does not cover is wrong rather than absent. + recorded before databases could be named. A name this client does not + cover raises `KeyError`, decided by `clients_covering` so that one + database answers a wrong name the same way a set does: provenance naming + another database is wrong rather than absent. """ - if not self.covers_multiple: - return self if source is None: - return None - (owner,) = await self.clients_for([source]) + return None if self.covers_multiple else self + (owner,) = await self.clients_covering([source]) return owner def _single_session(self, operation: str) -> SingleDatabaseSession: diff --git a/tests/chat/test_chat_app.py b/tests/chat/test_chat_app.py index 70d8c7e3..2da58fca 100644 --- a/tests/chat/test_chat_app.py +++ b/tests/chat/test_chat_app.py @@ -625,6 +625,25 @@ async def test_visual_grounding_uses_the_database_holding_the_citation(tmp_path) assert push.await_args.args[0].client is owner +class TestLendingTheClient: + @pytest.mark.asyncio + async def test_mounting_lends_its_client_to_every_capability(self, temp_db_path): + """Capabilities are built before the client exists, so each reads through + the one the app opened rather than opening its own.""" + client = _make_mock_client() + app, _ = _make_app(temp_db_path, client) + + with ( + patch("haiku.rag.chat.app.HaikuRAG") as stub_rag, + _covering_returns(stub_rag, client), + ): + async with app.run_test(): + borrowed = [c.borrowed_rag for c in app._capabilities] + + assert borrowed == [client] * len(app._capabilities) + assert borrowed + + class TestDocumentSelectionIdentity: """Two documents can share a title, within a corpus and across databases, so the selection is by id and the label says which database.""" diff --git a/tests/multi_db/test_capabilities.py b/tests/multi_db/test_capabilities.py index 8524bf3a..37425fb1 100644 --- a/tests/multi_db/test_capabilities.py +++ b/tests/multi_db/test_capabilities.py @@ -203,9 +203,9 @@ class TestCollectionIdentityForTheModel: class TestLendingANamedClient: @pytest.mark.asyncio async def test_a_lent_named_client_names_the_citation(self, tmp_path): - """The chat TUI builds capabilities before its client exists and lends - it on mount, so what a citation records is the lent client's database - and not whatever scope the capability was constructed with.""" + """What a citation records is the lent client's database, not the scope + the capability was constructed with. That chat lends its client is + `TestLendingTheClient` in `tests/chat/test_chat_app.py`.""" from tests.capabilities.test_capabilities import Deps, make_context config = _config(tmp_path, ["alpha", "beta"]) diff --git a/tests/multi_db/test_scope.py b/tests/multi_db/test_scope.py index 82966e55..c37dbfd6 100644 --- a/tests/multi_db/test_scope.py +++ b/tests/multi_db/test_scope.py @@ -236,7 +236,26 @@ class TestPlacingADatabase: async def test_a_client_reading_one_database_is_its_own_reader(self, temp_db_path): async with HaikuRAG(temp_db_path, create=True) as rag: assert await rag.reader_for(None) is rag - assert await rag.reader_for("anything") is rag + + @pytest.mark.asyncio + async def test_one_database_refuses_a_name_it_does_not_cover(self, tmp_path): + """Answering with itself would hand back the wrong database's reader for + a citation that named another.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha one"]) + await _seed(config, "beta", ["beta one"]) + + async with HaikuRAG(config=config, sources=["alpha"]) as alpha: + assert await alpha.reader_for("alpha") is alpha + with pytest.raises(KeyError, match="beta"): + await alpha.reader_for("beta") + + @pytest.mark.asyncio + async def test_an_unnamed_database_refuses_any_name(self, temp_db_path): + """Nothing names it, so no name can be the one it covers.""" + async with HaikuRAG(temp_db_path, create=True) as rag: + with pytest.raises(KeyError, match="single unnamed database"): + await rag.reader_for("anything") @pytest.mark.asyncio async def test_a_set_cannot_place_evidence_that_names_no_database(self, tmp_path):