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.
This commit is contained in:
Yiorgis Gozadinos 2026-08-27 14:49:40 +03:00
parent 45a5e68da5
commit d93cb5c891
No known key found for this signature in database
4 changed files with 48 additions and 11 deletions

View file

@ -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:

View file

@ -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."""

View file

@ -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"])

View file

@ -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):