Open read CLI verbs read-only

This commit is contained in:
Yiorgis Gozadinos 2026-06-05 10:48:49 +03:00
parent 0e3d53791f
commit 414be551fb
No known key found for this signature in database
4 changed files with 70 additions and 10 deletions

View file

@ -8,6 +8,7 @@
### Fixed
- Opening a database no longer writes to it: reads no longer rewrite the stored embedding settings or change the stored version, and the version is never downgraded. Embedding provider/name drift (matching `vector_dim`) warns on read-only opens and raises `ConfigMismatchError` on writable opens; reconcile with `rebuild --set-embedder`.
- Read CLI verbs (`list`, `get`, `search`, `visualize`, `ask`, `analyze`, `inspect`, `chat`, `info`, `history`) open the database read-only.
## [0.54.0] - 2026-06-04

View file

@ -276,7 +276,7 @@ class HaikuRAGApp: # pragma: no cover
async with HaikuRAG(
db_path=self.db_path,
config=self.config,
read_only=self.read_only,
read_only=True,
before=self.before,
) as self.client:
documents = await self.client.list_documents(filter=filter)
@ -328,7 +328,7 @@ class HaikuRAGApp: # pragma: no cover
async with HaikuRAG(
db_path=self.db_path,
config=self.config,
read_only=self.read_only,
read_only=True,
before=self.before,
) as self.client:
doc = await self.client.get_document_by_id(doc_id)
@ -385,7 +385,7 @@ class HaikuRAGApp: # pragma: no cover
async with HaikuRAG(
db_path=self.db_path,
config=self.config,
read_only=self.read_only,
read_only=True,
before=self.before,
) as self.client:
results = await self.client.search(
@ -407,7 +407,7 @@ class HaikuRAGApp: # pragma: no cover
async with HaikuRAG(
db_path=self.db_path,
config=self.config,
read_only=self.read_only,
read_only=True,
before=self.before,
) as self.client:
chunk = await self.client.get_chunk_by_id(chunk_id)
@ -451,7 +451,7 @@ class HaikuRAGApp: # pragma: no cover
async with HaikuRAG(
db_path=self.db_path,
config=self.config,
read_only=self.read_only,
read_only=True,
before=self.before,
) as self.client:
answer, citations = await self.client.ask(question, filter=filter)
@ -479,7 +479,7 @@ class HaikuRAGApp: # pragma: no cover
async with HaikuRAG(
db_path=self.db_path,
config=self.config,
read_only=self.read_only,
read_only=True,
before=self.before,
) as self.client:
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
@ -566,6 +566,7 @@ class HaikuRAGApp: # pragma: no cover
config=self.config,
skip_validation=True,
skip_migration_check=True,
read_only=self.read_only,
) as store:
return await store.migrate()

View file

@ -406,7 +406,7 @@ def analyze( # pragma: no cover
@_cli.command("settings", help="Display current configuration settings")
def settings(): # pragma: no cover
config = get_config()
app = HaikuRAGApp(db_path=Path(), config=config)
app = HaikuRAGApp(db_path=Path(), config=config, read_only=True)
app.show_settings()
@ -612,7 +612,7 @@ def history( # pragma: no cover
@_cli.command("download-models", help="Download Docling and Ollama models per config")
def download_models_cmd(): # pragma: no cover
app = HaikuRAGApp(db_path=Path(), config=get_config())
app = HaikuRAGApp(db_path=Path(), config=get_config(), read_only=True)
try:
asyncio.run(app.download_models())
except Exception as e:
@ -636,7 +636,7 @@ def inspect( # pragma: no cover
raise typer.Exit(1) from e
db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb"
run_inspector(db_path, read_only=_read_only, before=_before)
run_inspector(db_path, read_only=True, before=_before)
@_cli.command("chat", help="Launch interactive chat TUI for conversational RAG")
@ -666,7 +666,7 @@ def chat( # pragma: no cover
run_chat(
db_path,
read_only=_read_only,
read_only=True,
before=_before,
model=model,
skills=skills,

View file

@ -298,3 +298,61 @@ class TestClientReadOnly:
async with HaikuRAG(temp_db_path, read_only=True) as client:
docs = await client.list_documents()
assert len(docs) == 1
class TestAppReadVerbsDoNotWrite:
@pytest.mark.asyncio
async def test_read_verb_leaves_settings_and_version_unchanged_on_drift(
self, temp_db_path
):
"""A read CLI verb opens read-only: drift warns but never writes."""
from haiku.rag.app import HaikuRAGApp
from haiku.rag.config import AppConfig
async with Store(temp_db_path, create=True) as store:
stored_name_before = (
await SettingsRepository(store).get_current_settings()
)["embeddings"]["model"]["name"]
version_before = await store.get_haiku_version()
drift = AppConfig()
drift.embeddings.model.name = "different-model"
# list is a read verb — must open read-only and not raise on drift
app = HaikuRAGApp(db_path=temp_db_path, config=drift)
await app.list_documents()
async with Store(temp_db_path, skip_validation=True, read_only=True) as store:
stored_name_after = (
await SettingsRepository(store).get_current_settings()
)["embeddings"]["model"]["name"]
version_after = await store.get_haiku_version()
assert stored_name_after == stored_name_before
assert version_after == version_before
@pytest.mark.asyncio
async def test_write_verb_raises_on_drift_without_writing(self, temp_db_path):
"""A write CLI verb opens writable: drift raises before any write."""
from haiku.rag.app import HaikuRAGApp
from haiku.rag.config import AppConfig
from haiku.rag.store.repositories.settings import ConfigMismatchError
async with Store(temp_db_path, create=True) as store:
stored_name_before = (
await SettingsRepository(store).get_current_settings()
)["embeddings"]["model"]["name"]
drift = AppConfig()
drift.embeddings.model.name = "different-model"
app = HaikuRAGApp(db_path=temp_db_path, config=drift)
with pytest.raises(ConfigMismatchError):
await app.add_document_from_text("hello")
async with Store(temp_db_path, skip_validation=True, read_only=True) as store:
stored_name_after = (
await SettingsRepository(store).get_current_settings()
)["embeddings"]["model"]["name"]
assert stored_name_after == stored_name_before