Add global --read-only mode in CLI
This commit is contained in:
parent
1a8f72f99a
commit
98727ad080
7 changed files with 235 additions and 92 deletions
|
|
@ -1,6 +1,14 @@
|
|||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Read-Only Mode**: Global `--read-only` CLI flag for safe database access without modifications
|
||||
- Blocks all write operations at the Store layer
|
||||
- Skips database upgrades and settings saves on open
|
||||
- Excludes write tools (`add_document_*`, `delete_document`) from MCP server
|
||||
- Disables file monitor with warning when `--read-only` is used with `serve --monitor`
|
||||
|
||||
## [0.21.0] - 2025-12-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -35,9 +35,12 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class HaikuRAGApp:
|
||||
def __init__(self, db_path: Path, config: AppConfig = Config):
|
||||
def __init__(
|
||||
self, db_path: Path, config: AppConfig = Config, read_only: bool = False
|
||||
):
|
||||
self.db_path = db_path
|
||||
self.config = config
|
||||
self.read_only = read_only
|
||||
self.console = Console()
|
||||
|
||||
async def init(self):
|
||||
|
|
@ -213,13 +216,17 @@ class HaikuRAGApp:
|
|||
)
|
||||
|
||||
async def list_documents(self, filter: str | None = None):
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
documents = await self.client.list_documents(filter=filter)
|
||||
for doc in documents:
|
||||
self._rich_print_document(doc, truncate=True)
|
||||
|
||||
async def add_document_from_text(self, text: str, metadata: dict | None = None):
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
doc = await self.client.create_document(text, metadata=metadata)
|
||||
self._rich_print_document(doc, truncate=True)
|
||||
self.console.print(
|
||||
|
|
@ -229,7 +236,9 @@ class HaikuRAGApp:
|
|||
async def add_document_from_source(
|
||||
self, source: str, title: str | None = None, metadata: dict | None = None
|
||||
):
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
result = await self.client.create_document_from_source(
|
||||
source, title=title, metadata=metadata
|
||||
)
|
||||
|
|
@ -246,7 +255,9 @@ class HaikuRAGApp:
|
|||
)
|
||||
|
||||
async def get_document(self, doc_id: str):
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
doc = await self.client.get_document_by_id(doc_id)
|
||||
if doc is None:
|
||||
self.console.print(f"[red]Document with id {doc_id} not found.[/red]")
|
||||
|
|
@ -254,7 +265,9 @@ class HaikuRAGApp:
|
|||
self._rich_print_document(doc, truncate=False)
|
||||
|
||||
async def delete_document(self, doc_id: str):
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
deleted = await self.client.delete_document(doc_id)
|
||||
if deleted:
|
||||
self.console.print(
|
||||
|
|
@ -268,7 +281,9 @@ class HaikuRAGApp:
|
|||
async def search(
|
||||
self, query: str, limit: int | None = None, filter: str | None = None
|
||||
):
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
results = await self.client.search(query, limit=limit, filter=filter)
|
||||
if not results:
|
||||
self.console.print("[yellow]No results found.[/yellow]")
|
||||
|
|
@ -280,7 +295,9 @@ class HaikuRAGApp:
|
|||
"""Display visual grounding images for a chunk."""
|
||||
from textual_image.renderable import Image as RichImage
|
||||
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
chunk = await self.client.chunk_repository.get_by_id(chunk_id)
|
||||
if not chunk:
|
||||
self.console.print(f"[red]Chunk with id {chunk_id} not found.[/red]")
|
||||
|
|
@ -325,7 +342,9 @@ class HaikuRAGApp:
|
|||
verbose: Show verbose output
|
||||
filter: SQL WHERE clause to filter documents
|
||||
"""
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as self.client:
|
||||
try:
|
||||
citations = []
|
||||
if deep:
|
||||
|
|
@ -394,7 +413,9 @@ class HaikuRAGApp:
|
|||
verbose: Show AG-UI event stream during execution
|
||||
filter: SQL WHERE clause to filter documents
|
||||
"""
|
||||
async with HaikuRAG(db_path=self.db_path, config=self.config) as client:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, read_only=self.read_only
|
||||
) as client:
|
||||
try:
|
||||
self.console.print("[bold cyan]Starting research[/bold cyan]")
|
||||
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
||||
|
|
@ -485,7 +506,10 @@ class HaikuRAGApp:
|
|||
|
||||
async def rebuild(self, mode: RebuildMode = RebuildMode.FULL):
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, skip_validation=True
|
||||
db_path=self.db_path,
|
||||
config=self.config,
|
||||
skip_validation=True,
|
||||
read_only=self.read_only,
|
||||
) as client:
|
||||
try:
|
||||
documents = await client.list_documents()
|
||||
|
|
@ -521,7 +545,10 @@ class HaikuRAGApp:
|
|||
"""Run database maintenance: optimize and cleanup table history."""
|
||||
try:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, skip_validation=True
|
||||
db_path=self.db_path,
|
||||
config=self.config,
|
||||
skip_validation=True,
|
||||
read_only=self.read_only,
|
||||
) as client:
|
||||
await client.vacuum()
|
||||
self.console.print(
|
||||
|
|
@ -534,7 +561,10 @@ class HaikuRAGApp:
|
|||
"""Create vector index on the chunks table."""
|
||||
try:
|
||||
async with HaikuRAG(
|
||||
db_path=self.db_path, config=self.config, skip_validation=True
|
||||
db_path=self.db_path,
|
||||
config=self.config,
|
||||
skip_validation=True,
|
||||
read_only=self.read_only,
|
||||
) as client:
|
||||
row_count = client.store.chunks_table.count_rows()
|
||||
self.console.print(f"Chunks in database: {row_count}")
|
||||
|
|
@ -704,18 +734,27 @@ class HaikuRAGApp:
|
|||
enable_agui: bool = False,
|
||||
):
|
||||
"""Start the server with selected services."""
|
||||
async with HaikuRAG(self.db_path, config=self.config) as client:
|
||||
async with HaikuRAG(
|
||||
self.db_path, config=self.config, read_only=self.read_only
|
||||
) as client:
|
||||
tasks = []
|
||||
|
||||
# Start file monitor if enabled
|
||||
# Start file monitor if enabled (not available in read-only mode)
|
||||
if enable_monitor:
|
||||
monitor = FileWatcher(client=client, config=self.config)
|
||||
monitor_task = asyncio.create_task(monitor.observe())
|
||||
tasks.append(monitor_task)
|
||||
if self.read_only:
|
||||
logger.warning(
|
||||
"File monitor disabled: cannot monitor files in read-only mode"
|
||||
)
|
||||
else:
|
||||
monitor = FileWatcher(client=client, config=self.config)
|
||||
monitor_task = asyncio.create_task(monitor.observe())
|
||||
tasks.append(monitor_task)
|
||||
|
||||
# Start MCP server if enabled
|
||||
if enable_mcp:
|
||||
server = create_mcp_server(self.db_path, config=self.config)
|
||||
server = create_mcp_server(
|
||||
self.db_path, config=self.config, read_only=self.read_only
|
||||
)
|
||||
|
||||
async def run_mcp():
|
||||
if mcp_transport == "stdio":
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ cli = typer.Typer(
|
|||
context_settings={"help_option_names": ["-h", "--help"]}, no_args_is_help=True
|
||||
)
|
||||
|
||||
# Module-level read-only flag set by callback
|
||||
_read_only: bool = False
|
||||
|
||||
|
||||
def create_app(db: Path | None = None) -> HaikuRAGApp:
|
||||
"""Create HaikuRAGApp with loaded config and resolved database path.
|
||||
|
|
@ -38,7 +41,7 @@ def create_app(db: Path | None = None) -> HaikuRAGApp:
|
|||
"""
|
||||
config = get_config()
|
||||
db_path = db if db else config.storage.data_dir / "haiku.rag.lancedb"
|
||||
return HaikuRAGApp(db_path=db_path, config=config)
|
||||
return HaikuRAGApp(db_path=db_path, config=config, read_only=_read_only)
|
||||
|
||||
|
||||
async def check_version():
|
||||
|
|
@ -72,8 +75,15 @@ def main(
|
|||
"--config",
|
||||
help="Path to YAML configuration file",
|
||||
),
|
||||
read_only: bool = typer.Option(
|
||||
False,
|
||||
"--read-only",
|
||||
help="Open database in read-only mode",
|
||||
),
|
||||
):
|
||||
"""haiku.rag CLI - Vector database RAG system"""
|
||||
global _read_only
|
||||
_read_only = read_only
|
||||
# Load config from --config, local folder, or default directory
|
||||
config_path = find_config_file(cli_path=config)
|
||||
if config_path:
|
||||
|
|
@ -353,7 +363,7 @@ def research(
|
|||
from haiku.rag.cli_chat import interactive_research
|
||||
from haiku.rag.client import HaikuRAG
|
||||
|
||||
client = HaikuRAG(db_path=app.db_path, config=app.config)
|
||||
client = HaikuRAG(db_path=app.db_path, config=app.config, read_only=_read_only)
|
||||
try:
|
||||
interactive_research(
|
||||
client=client,
|
||||
|
|
@ -521,7 +531,7 @@ def inspect(
|
|||
raise typer.Exit(1) from e
|
||||
|
||||
db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb"
|
||||
run_inspector(db_path)
|
||||
run_inspector(db_path, read_only=_read_only)
|
||||
|
||||
|
||||
@cli.command(
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class HaikuRAG:
|
|||
config: AppConfig = Config,
|
||||
skip_validation: bool = False,
|
||||
create: bool = False,
|
||||
read_only: bool = False,
|
||||
):
|
||||
"""Initialize the RAG client with a database path.
|
||||
|
||||
|
|
@ -68,6 +69,7 @@ class HaikuRAG:
|
|||
config: Configuration to use. Defaults to global Config.
|
||||
skip_validation: Whether to skip configuration validation on database load.
|
||||
create: Whether to create the database if it doesn't exist.
|
||||
read_only: Whether to open the database in read-only mode.
|
||||
"""
|
||||
self._config = config
|
||||
if db_path is None:
|
||||
|
|
@ -77,10 +79,16 @@ class HaikuRAG:
|
|||
config=self._config,
|
||||
skip_validation=skip_validation,
|
||||
create=create,
|
||||
read_only=read_only,
|
||||
)
|
||||
self.document_repository = DocumentRepository(self.store)
|
||||
self.chunk_repository = ChunkRepository(self.store)
|
||||
|
||||
@property
|
||||
def is_read_only(self) -> bool:
|
||||
"""Whether the client is in read-only mode."""
|
||||
return self.store.is_read_only
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -74,9 +74,10 @@ class InspectorApp(App): # type: ignore[misc] # pragma: no cover
|
|||
Binding("c", "show_context", "Context", show=True),
|
||||
]
|
||||
|
||||
def __init__(self, db_path: Path):
|
||||
def __init__(self, db_path: Path, read_only: bool = False):
|
||||
super().__init__()
|
||||
self.db_path = db_path
|
||||
self.read_only = read_only
|
||||
self.client: HaikuRAG | None = None
|
||||
|
||||
def compose(self) -> "ComposeResult":
|
||||
|
|
@ -90,7 +91,9 @@ class InspectorApp(App): # type: ignore[misc] # pragma: no cover
|
|||
async def on_mount(self) -> None:
|
||||
"""Initialize the app when mounted."""
|
||||
config = get_config()
|
||||
self.client = HaikuRAG(db_path=self.db_path, config=config)
|
||||
self.client = HaikuRAG(
|
||||
db_path=self.db_path, config=config, read_only=self.read_only
|
||||
)
|
||||
await self.client.__aenter__()
|
||||
|
||||
# Load initial documents
|
||||
|
|
@ -229,15 +232,18 @@ class InspectorApp(App): # type: ignore[misc] # pragma: no cover
|
|||
await self._switch_modal(ContextModal(chunk=chunk, client=self.client))
|
||||
|
||||
|
||||
def run_inspector(db_path: Path | None = None) -> None: # pragma: no cover
|
||||
def run_inspector(
|
||||
db_path: Path | None = None, read_only: bool = False
|
||||
) -> None: # pragma: no cover
|
||||
"""Run the inspector TUI.
|
||||
|
||||
Args:
|
||||
db_path: Path to the LanceDB database. If None, uses default from config.
|
||||
read_only: Whether to open the database in read-only mode.
|
||||
"""
|
||||
config = get_config()
|
||||
if db_path is None:
|
||||
db_path = config.storage.data_dir / "haiku.rag.lancedb"
|
||||
|
||||
app = InspectorApp(db_path)
|
||||
app = InspectorApp(db_path, read_only=read_only)
|
||||
app.run()
|
||||
|
|
|
|||
|
|
@ -21,70 +21,91 @@ class DocumentResult(BaseModel):
|
|||
updated_at: str
|
||||
|
||||
|
||||
def create_mcp_server(db_path: Path, config: AppConfig = Config) -> FastMCP:
|
||||
"""Create an MCP server with the specified database path."""
|
||||
def create_mcp_server(
|
||||
db_path: Path, config: AppConfig = Config, read_only: bool = False
|
||||
) -> FastMCP:
|
||||
"""Create an MCP server with the specified database path.
|
||||
|
||||
Args:
|
||||
db_path: Path to the database file.
|
||||
config: Configuration to use.
|
||||
read_only: If True, write tools (add_document_*, delete_document) are not registered.
|
||||
"""
|
||||
mcp = FastMCP("haiku-rag")
|
||||
|
||||
@mcp.tool()
|
||||
async def add_document_from_file(
|
||||
file_path: str,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
title: str | None = None,
|
||||
) -> str | None:
|
||||
"""Add a document to the RAG system from a file path."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
result = await rag.create_document_from_source(
|
||||
Path(file_path), title=title, metadata=metadata or {}
|
||||
)
|
||||
# Handle both single document and list of documents (directories)
|
||||
if isinstance(result, list):
|
||||
return result[0].id if result else None
|
||||
return result.id
|
||||
except Exception:
|
||||
return None
|
||||
# Write tools - only registered when not in read-only mode
|
||||
if not read_only:
|
||||
|
||||
@mcp.tool()
|
||||
async def add_document_from_url(
|
||||
url: str, metadata: dict[str, Any] | None = None, title: str | None = None
|
||||
) -> str | None:
|
||||
"""Add a document to the RAG system from a URL."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
result = await rag.create_document_from_source(
|
||||
url, title=title, metadata=metadata or {}
|
||||
)
|
||||
# Handle both single document and list of documents
|
||||
if isinstance(result, list):
|
||||
return result[0].id if result else None
|
||||
return result.id
|
||||
except Exception:
|
||||
return None
|
||||
@mcp.tool()
|
||||
async def add_document_from_file(
|
||||
file_path: str,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
title: str | None = None,
|
||||
) -> str | None:
|
||||
"""Add a document to the RAG system from a file path."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
result = await rag.create_document_from_source(
|
||||
Path(file_path), title=title, metadata=metadata or {}
|
||||
)
|
||||
# Handle both single document and list of documents (directories)
|
||||
if isinstance(result, list):
|
||||
return result[0].id if result else None
|
||||
return result.id
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@mcp.tool()
|
||||
async def add_document_from_text(
|
||||
content: str,
|
||||
uri: str | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
title: str | None = None,
|
||||
) -> str | None:
|
||||
"""Add a document to the RAG system from text content."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
document = await rag.create_document(
|
||||
content, uri, title=title, metadata=metadata or {}
|
||||
)
|
||||
return document.id
|
||||
except Exception:
|
||||
return None
|
||||
@mcp.tool()
|
||||
async def add_document_from_url(
|
||||
url: str, metadata: dict[str, Any] | None = None, title: str | None = None
|
||||
) -> str | None:
|
||||
"""Add a document to the RAG system from a URL."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
result = await rag.create_document_from_source(
|
||||
url, title=title, metadata=metadata or {}
|
||||
)
|
||||
# Handle both single document and list of documents
|
||||
if isinstance(result, list):
|
||||
return result[0].id if result else None
|
||||
return result.id
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@mcp.tool()
|
||||
async def add_document_from_text(
|
||||
content: str,
|
||||
uri: str | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
title: str | None = None,
|
||||
) -> str | None:
|
||||
"""Add a document to the RAG system from text content."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
document = await rag.create_document(
|
||||
content, uri, title=title, metadata=metadata or {}
|
||||
)
|
||||
return document.id
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_document(document_id: str) -> bool:
|
||||
"""Delete a document by its ID."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
return await rag.delete_document(document_id)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Read tools - always registered
|
||||
@mcp.tool()
|
||||
async def search_documents(
|
||||
query: str, limit: int | None = None
|
||||
) -> list[SearchResult]:
|
||||
"""Search the RAG system for documents using hybrid search (vector similarity + full-text search)."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
async with HaikuRAG(db_path, config=config, read_only=read_only) as rag:
|
||||
return await rag.search(query, limit=limit)
|
||||
except Exception:
|
||||
return []
|
||||
|
|
@ -93,7 +114,7 @@ def create_mcp_server(db_path: Path, config: AppConfig = Config) -> FastMCP:
|
|||
async def get_document(document_id: str) -> DocumentResult | None:
|
||||
"""Get a document by its ID."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
async with HaikuRAG(db_path, config=config, read_only=read_only) as rag:
|
||||
document = await rag.get_document_by_id(document_id)
|
||||
|
||||
if document is None:
|
||||
|
|
@ -128,7 +149,7 @@ def create_mcp_server(db_path: Path, config: AppConfig = Config) -> FastMCP:
|
|||
List of DocumentResult instances matching the criteria.
|
||||
"""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
async with HaikuRAG(db_path, config=config, read_only=read_only) as rag:
|
||||
documents = await rag.list_documents(limit, offset, filter)
|
||||
|
||||
return [
|
||||
|
|
@ -146,15 +167,6 @@ def create_mcp_server(db_path: Path, config: AppConfig = Config) -> FastMCP:
|
|||
except Exception:
|
||||
return []
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_document(document_id: str) -> bool:
|
||||
"""Delete a document by its ID."""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
return await rag.delete_document(document_id)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@mcp.tool()
|
||||
async def ask_question(
|
||||
question: str,
|
||||
|
|
@ -172,7 +184,7 @@ def create_mcp_server(db_path: Path, config: AppConfig = Config) -> FastMCP:
|
|||
The answer as a string.
|
||||
"""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
async with HaikuRAG(db_path, config=config, read_only=read_only) as rag:
|
||||
if deep:
|
||||
from haiku.rag.graph.research.dependencies import ResearchContext
|
||||
from haiku.rag.graph.research.graph import build_research_graph
|
||||
|
|
@ -222,7 +234,7 @@ def create_mcp_server(db_path: Path, config: AppConfig = Config) -> FastMCP:
|
|||
from haiku.rag.graph.research.graph import build_research_graph
|
||||
from haiku.rag.graph.research.state import ResearchDeps, ResearchState
|
||||
|
||||
async with HaikuRAG(db_path, config=config) as rag:
|
||||
async with HaikuRAG(db_path, config=config, read_only=read_only) as rag:
|
||||
graph = build_research_graph(config=config)
|
||||
context = ResearchContext(original_question=question)
|
||||
state = ResearchState.from_config(context=context, config=config)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.store import ReadOnlyError, Store
|
||||
from haiku.rag.store.models import Chunk, Document
|
||||
from haiku.rag.store.repositories.chunk import ChunkRepository
|
||||
|
|
@ -227,3 +228,62 @@ class TestSettingsRepositoryReadOnly:
|
|||
with pytest.raises(ReadOnlyError):
|
||||
repo.save_current_settings()
|
||||
store.close()
|
||||
|
||||
|
||||
class TestClientReadOnly:
|
||||
def test_client_default_is_not_read_only(self, temp_db_path):
|
||||
"""Client defaults to not read-only."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
assert client.is_read_only is False
|
||||
client.close()
|
||||
|
||||
def test_client_can_be_created_read_only(self, temp_db_path):
|
||||
"""Client can be created with read_only=True."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
client.close()
|
||||
|
||||
client = HaikuRAG(temp_db_path, read_only=True)
|
||||
assert client.is_read_only is True
|
||||
client.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_create_document_raises_when_read_only(self, temp_db_path):
|
||||
"""Client.create_document() raises ReadOnlyError when read_only=True."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
client.close()
|
||||
|
||||
async with HaikuRAG(temp_db_path, read_only=True) as client:
|
||||
with pytest.raises(ReadOnlyError):
|
||||
await client.create_document("test content")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_delete_document_raises_when_read_only(self, temp_db_path):
|
||||
"""Client.delete_document() raises ReadOnlyError when read_only=True."""
|
||||
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||
doc = await client.create_document("test content")
|
||||
assert doc.id is not None
|
||||
doc_id = doc.id
|
||||
|
||||
async with HaikuRAG(temp_db_path, read_only=True) as client:
|
||||
with pytest.raises(ReadOnlyError):
|
||||
await client.delete_document(doc_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_search_works_when_read_only(self, temp_db_path):
|
||||
"""Client.search() works in read-only mode."""
|
||||
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||
await client.create_document("test content about cats")
|
||||
|
||||
async with HaikuRAG(temp_db_path, read_only=True) as client:
|
||||
results = await client.search("cats")
|
||||
assert len(results) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_list_documents_works_when_read_only(self, temp_db_path):
|
||||
"""Client.list_documents() works in read-only mode."""
|
||||
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||
await client.create_document("test content")
|
||||
|
||||
async with HaikuRAG(temp_db_path, read_only=True) as client:
|
||||
docs = await client.list_documents()
|
||||
assert len(docs) == 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue