haiku.rag/tests/test_database_scope.py
Yiorgis Gozadinos d367b1eb5a
Resolve the databases a command works on, once
The CLI decides only what it knows — that --db and --database are the same
thing said twice, and whether a command reads more than one — and hands the
resolved scope down. Nothing rewrites the configuration, so a named database
keeps the name results and citations carry, and a remote one opens the URI
it was configured with rather than the local path standing in for it.

HaikuRAGApp, ChatApp and InspectorApp take that scope and nothing else.
Selection reaches the client through a private constructor, so the public
signature still takes a path or names.
2026-08-25 15:48:47 +03:00

206 lines
8.2 KiB
Python

from pathlib import Path
import pytest
from haiku.rag.client.scope import DatabaseRef, DatabaseScope
from haiku.rag.client.session import default_db_path
from haiku.rag.config.models import AppConfig, LanceDBConfig, StorageConfig
from haiku.rag.store.exceptions import AmbiguousDatabaseError
def _config(**kwargs) -> AppConfig:
return AppConfig(lancedb=LanceDBConfig(**kwargs))
class TestResolution:
"""One selector at most, and the same answer wherever it is asked."""
def test_a_name_and_a_path_together_are_refused(self):
config = _config(databases={"alpha": "/data/alpha.lancedb"})
with pytest.raises(AmbiguousDatabaseError, match="pass one of them"):
DatabaseScope.resolve(
config, database_name="alpha", database_path=Path("/data/other.lancedb")
)
def test_a_path_names_one_unnamed_database(self):
"""A path says which database, not what it is called, even where the
configuration would have named it."""
config = _config(databases={"alpha": "/data/alpha.lancedb"})
scope = DatabaseScope.resolve(config, database_path=Path("/data/other.lancedb"))
assert scope.databases == (DatabaseRef.at("/data/other.lancedb"),)
assert scope.names == ()
assert not scope.covers_multiple
def test_a_named_database_keeps_its_name(self):
config = _config(databases={"alpha": "/data/alpha.lancedb", "beta": "b://b"})
scope = DatabaseScope.resolve(config, database_name="beta")
assert scope.databases == (DatabaseRef("beta", "b://b", None),)
assert scope.names == ("beta",)
def test_an_unknown_name_is_refused(self):
config = _config(databases={"alpha": "/data/alpha.lancedb"})
with pytest.raises(AmbiguousDatabaseError, match="unknown database 'nope'"):
DatabaseScope.resolve(config, database_name="nope")
def test_no_selector_covers_the_configured_set_in_order(self):
config = _config(
databases={"beta": "/data/b.lancedb", "alpha": "/data/a.lancedb"}
)
scope = DatabaseScope.resolve(config)
assert scope.names == ("beta", "alpha")
assert scope.covers_multiple
def test_a_configured_set_of_one_is_still_a_named_database(self):
"""Its name is what results and citations carry, so it survives."""
config = _config(databases={"alpha": "/data/alpha.lancedb"})
scope = DatabaseScope.resolve(config)
assert scope.databases == (
DatabaseRef.configured("alpha", "/data/alpha.lancedb"),
)
assert not scope.covers_multiple
def test_a_bare_uri_is_one_unnamed_database(self):
scope = DatabaseScope.resolve(_config(uri="s3://bucket/one.lancedb"))
assert scope.databases == (DatabaseRef(None, "s3://bucket/one.lancedb", None),)
def test_a_path_selects_the_database_over_a_configured_uri(self):
"""`--db` exists to override what is configured, and the configuration
derived from the ref is what makes the connection follow it."""
config = _config(uri="s3://bucket/one.lancedb")
scope = DatabaseScope.resolve(config, database_path=Path("/data/local"))
[ref] = scope.databases
assert ref.db_path == Path("/data/local")
one, _ = ref.connection(config)
assert one.lancedb.uri == ""
def test_nothing_configured_falls_back_to_the_data_directory(self, tmp_path):
config = AppConfig(storage=StorageConfig(data_dir=tmp_path))
scope = DatabaseScope.resolve(config)
assert scope.databases == (DatabaseRef.at(tmp_path / "haiku.rag.lancedb"),)
def test_the_environment_is_not_consulted(self, monkeypatch, tmp_path):
"""HAIKU_RAG_DB is honoured by the capability entry point alone. Reading it
here would change what every other caller opens."""
monkeypatch.setenv("HAIKU_RAG_DB", "/data/from-the-environment.lancedb")
config = _config(databases={"alpha": "/data/alpha.lancedb"})
scope = DatabaseScope.resolve(config)
assert scope.names == ("alpha",)
def test_a_path_is_never_reinterpreted_as_a_uri(self):
"""A caller naming a path means that path. Sending it back through the
configured-location rules would let a scheme turn it into a URI."""
scope = DatabaseScope.resolve(
_config(), database_path="s3://bucket/looks-like-a-uri.lancedb"
)
[ref] = scope.databases
assert ref.db_path == Path("s3://bucket/looks-like-a-uri.lancedb")
assert ref.uri == ""
def test_a_configured_location_with_a_scheme_is_a_uri(self):
"""A configured value is a URI or a path depending on its scheme, which is
what makes it different from a path the caller gave."""
config = _config(databases={"alpha": "s3://bucket/alpha.lancedb"})
[ref] = DatabaseScope.resolve(config).databases
assert ref.uri == "s3://bucket/alpha.lancedb"
assert ref.db_path is None
def test_a_database_is_a_uri_or_a_path(self):
"""Both would silently ignore the path; neither fails later, when the
connection is derived and there is nothing to open.
The message names what it was given: this is a programming error raised
in the caller's own process, not one an operator or a model ever sees.
"""
with pytest.raises(ValueError, match="either a URI or a local path") as both:
DatabaseRef(None, "s3://bucket/a.lancedb", Path("/data/a.lancedb"))
assert "s3://bucket/a.lancedb" in str(both.value)
with pytest.raises(ValueError, match="either a URI or a local path") as neither:
DatabaseRef(None, "", None)
assert "db_path=None" in str(neither.value)
def test_a_scope_covers_at_least_one_database(self):
"""Every resolution reaches a database, and the sessions built from a
scope have no meaning without one."""
with pytest.raises(ValueError, match="at least one database"):
DatabaseScope(())
class TestConnectionDerivation:
"""Opening one of a set must not disturb the configuration it came from."""
def test_a_local_location_becomes_a_path(self):
config = _config(databases={"alpha": "/data/alpha.lancedb"})
[ref] = DatabaseScope.resolve(config).databases
one, db_path = ref.connection(config)
assert db_path == Path("/data/alpha.lancedb")
assert one.lancedb.uri == ""
assert one.lancedb.databases == {}
def test_a_uri_location_stays_a_uri(self):
config = _config(databases={"alpha": "s3://bucket/alpha.lancedb"})
[ref] = DatabaseScope.resolve(config).databases
one, db_path = ref.connection(config)
assert db_path is None
assert one.lancedb.uri == "s3://bucket/alpha.lancedb"
def test_the_original_configuration_is_untouched(self):
"""Rewriting it in place is what left downstream code unable to tell a set
had been named."""
config = _config(databases={"alpha": "/a.lancedb", "beta": "/b.lancedb"})
for ref in DatabaseScope.resolve(config).databases:
ref.connection(config)
assert config.lancedb.databases == {"alpha": "/a.lancedb", "beta": "/b.lancedb"}
assert config.lancedb.uri == ""
def test_each_derived_configuration_is_its_own_copy(self):
config = _config(databases={"alpha": "/a.lancedb", "beta": "s3://b/b.lancedb"})
alpha, beta = DatabaseScope.resolve(config).databases
one, _ = alpha.connection(config)
other, _ = beta.connection(config)
assert one is not other
assert one.lancedb.uri == ""
assert other.lancedb.uri == "s3://b/b.lancedb"
def test_a_database_behind_a_uri_has_no_path_of_its_own(tmp_path):
"""`connection` hands back no path for a URI, and the store still needs one:
the default stands in, and the URI is what decides where it connects."""
config = AppConfig(
storage=StorageConfig(data_dir=tmp_path),
lancedb=LanceDBConfig(databases={"alpha": "s3://bucket/alpha.lancedb"}),
)
[ref] = DatabaseScope.resolve(config).databases
one, db_path = ref.connection(config)
assert db_path is None
assert default_db_path(one) == tmp_path / "haiku.rag.lancedb"