Improve test coverage for cli, filter, registry, and migrations

These files were not touched by the recent performance and
correctness PRs but had coverage gaps. Adds tests for:

- CLI: serve, queue init/migrate, config loading, cli() entry point
  including MigrationRequiredError exit path
- filter: _default_supported_extensions, __call__ watchfiles callback,
  FileFilter with supported_extensions=None
- registry: resolve_adhoc_fetcher with bucket-less S3 URI
- migrations: pragma no-cover on unreachable schema upgrade path
  (no diff migrations exist until SCHEMA_VERSION > 1)
This commit is contained in:
Chris McDonough 2026-06-01 10:29:27 -04:00 committed by Yiorgis Gozadinos
parent 64f2b7b7d2
commit 1bf2505093
No known key found for this signature in database
4 changed files with 153 additions and 6 deletions

View file

@ -36,9 +36,7 @@ async def apply_migrations(conn: aiosqlite.Connection) -> int:
) )
else: else:
current = row[0] current = row[0]
if current < SCHEMA_VERSION: if current < SCHEMA_VERSION: # pragma: no cover - no migrations yet
# No diff migrations exist yet — future versions add UPDATE/ALTER
# statements between here and the version bump.
await _exec(conn, "UPDATE schema_version SET version = ?", SCHEMA_VERSION) await _exec(conn, "UPDATE schema_version SET version = ?", SCHEMA_VERSION)
await conn.commit() await conn.commit()

View file

@ -1,8 +1,9 @@
"""haiku-ingester run-batch CLI: echoes the batch report and exits non-zero """haiku-ingester CLI: exercises every subcommand via CliRunner with
when any job dead-letters. IngesterApp is patched so no real ingestion runs.""" IngesterApp / open_queue patched out so no real ingestion runs."""
from unittest.mock import AsyncMock from unittest.mock import AsyncMock, MagicMock
import pytest
from typer.testing import CliRunner from typer.testing import CliRunner
from haiku.rag.ingester.app import BatchReport from haiku.rag.ingester.app import BatchReport
@ -11,6 +12,9 @@ from haiku.rag.ingester.cli import _cli as cli
runner = CliRunner() runner = CliRunner()
# --- helpers ---
def _fake_app(report: BatchReport, monkeypatch) -> AsyncMock: def _fake_app(report: BatchReport, monkeypatch) -> AsyncMock:
fake = AsyncMock() fake = AsyncMock()
fake.run_batch.return_value = report fake.run_batch.return_value = report
@ -44,3 +48,118 @@ def test_run_batch_exits_nonzero_when_sweep_fails(monkeypatch):
assert result.exit_code == 1 assert result.exit_code == 1
assert "failed to sweep: docs" in result.output assert "failed to sweep: docs" in result.output
# --- serve ---
def test_serve_invokes_app(monkeypatch):
fake = AsyncMock()
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake)
result = runner.invoke(cli, ["serve", "--db", "x.lancedb", "--no-api"])
assert result.exit_code == 0
fake.serve.assert_awaited_once_with(api=False)
def test_serve_passes_host_and_port(monkeypatch):
fake = AsyncMock()
captured = {}
def _capture(**kwargs):
captured.update(kwargs)
return fake
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", _capture)
result = runner.invoke(
cli, ["serve", "--db", "x.lancedb", "--host", "0.0.0.0", "--port", "9999"]
)
assert result.exit_code == 0
assert captured["config"].ingester.api.host == "0.0.0.0"
assert captured["config"].ingester.api.port == 9999
# --- queue init / migrate ---
def test_queue_init(tmp_path, monkeypatch):
fake_conn = AsyncMock()
monkeypatch.setattr(
"haiku.rag.ingester.cli.open_queue", AsyncMock(return_value=fake_conn)
)
db_path = tmp_path / "queue.db"
result = runner.invoke(cli, ["queue", "init", "--queue", str(db_path)])
assert result.exit_code == 0
assert "initialized" in result.output
def test_queue_migrate(tmp_path, monkeypatch):
fake_conn = AsyncMock()
monkeypatch.setattr(
"haiku.rag.ingester.cli.open_queue", AsyncMock(return_value=fake_conn)
)
db_path = tmp_path / "queue.db"
result = runner.invoke(cli, ["queue", "migrate", "--queue", str(db_path)])
assert result.exit_code == 0
assert "up to date" in result.output
# --- config loading ---
def test_load_config_with_explicit_path(tmp_path):
from haiku.rag.ingester.cli import _load_config_with_override
config_file = tmp_path / "test.yaml"
config_file.write_text("embeddings:\n model:\n provider: ollama\n")
config = _load_config_with_override(config_file)
assert config is not None
def test_load_config_falls_back_to_default(monkeypatch):
from haiku.rag.ingester.cli import _load_config_with_override
monkeypatch.setattr("haiku.rag.ingester.cli.find_config_file", lambda _: None)
config = _load_config_with_override(None)
assert config is not None
# --- cli() entry point ---
def test_cli_entry_point(monkeypatch):
from haiku.rag.ingester.cli import cli as cli_entry
mock_cli = MagicMock()
monkeypatch.setattr("haiku.rag.ingester.cli._cli", mock_cli)
monkeypatch.setattr("haiku.rag.ingester.cli.configure_cli_logging", lambda: None)
monkeypatch.setattr("haiku.rag.telemetry.configure", lambda **_: None)
cli_entry()
mock_cli.assert_called_once()
def test_cli_entry_point_exits_on_migration_error(monkeypatch):
from haiku.rag.ingester.cli import cli as cli_entry
from haiku.rag.store.exceptions import MigrationRequiredError
monkeypatch.setattr(
"haiku.rag.ingester.cli._cli",
MagicMock(side_effect=MigrationRequiredError("need migration")),
)
monkeypatch.setattr("haiku.rag.ingester.cli.configure_cli_logging", lambda: None)
monkeypatch.setattr("haiku.rag.telemetry.configure", lambda **_: None)
with pytest.raises(SystemExit) as exc_info:
cli_entry()
assert exc_info.value.code == 1

View file

@ -0,0 +1,25 @@
from watchfiles import Change
from haiku.rag.ingester.sources.filter import FileFilter, _default_supported_extensions
def test_default_supported_extensions_returns_nonempty_list():
exts = _default_supported_extensions()
assert isinstance(exts, list)
assert len(exts) > 0
assert all(ext.startswith(".") for ext in exts)
def test_filter_uses_default_extensions_when_none():
f = FileFilter(supported_extensions=None)
assert len(f.extensions) > 0
def test_call_delegates_to_include_file_then_default_filter():
"""__call__ is the watchfiles callback entry point. It should reject
files that don't pass include_file and accept ones that do."""
f = FileFilter(supported_extensions=[".md"])
# DefaultFilter rejects dotfiles and common noise; a normal .md path passes.
assert f(Change.added, "/tmp/docs/readme.md") is True
# Wrong extension — include_file returns False before DefaultFilter runs.
assert f(Change.added, "/tmp/docs/readme.log") is False

View file

@ -44,6 +44,11 @@ def test_adhoc_resolves_s3_forwards_storage_options():
assert src.storage_options["endpoint"] == "http://seaweed:8333" assert src.storage_options["endpoint"] == "http://seaweed:8333"
def test_adhoc_s3_without_bucket_raises():
with pytest.raises(ValueError, match="Invalid S3 URI"):
resolve_adhoc_fetcher("s3:///key-only")
def test_adhoc_unknown_scheme_raises(): def test_adhoc_unknown_scheme_raises():
with pytest.raises(ValueError, match="No source adapter"): with pytest.raises(ValueError, match="No source adapter"):
resolve_adhoc_fetcher("ftp://example.com/x") resolve_adhoc_fetcher("ftp://example.com/x")