From 1bf2505093a399eae0ebbfb7989e1791eeab3983 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 1 Jun 2026 10:29:27 -0400 Subject: [PATCH] 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) --- .../haiku/rag/ingester/queue/migrations.py | 4 +- tests/ingester/test_cli.py | 125 +++++++++++++++++- tests/ingester/test_filter.py | 25 ++++ tests/ingester/test_resolve_fetcher.py | 5 + 4 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 tests/ingester/test_filter.py diff --git a/haiku_rag_slim/haiku/rag/ingester/queue/migrations.py b/haiku_rag_slim/haiku/rag/ingester/queue/migrations.py index 87be765a..ea37d49a 100644 --- a/haiku_rag_slim/haiku/rag/ingester/queue/migrations.py +++ b/haiku_rag_slim/haiku/rag/ingester/queue/migrations.py @@ -36,9 +36,7 @@ async def apply_migrations(conn: aiosqlite.Connection) -> int: ) else: current = row[0] - if current < SCHEMA_VERSION: - # No diff migrations exist yet — future versions add UPDATE/ALTER - # statements between here and the version bump. + if current < SCHEMA_VERSION: # pragma: no cover - no migrations yet await _exec(conn, "UPDATE schema_version SET version = ?", SCHEMA_VERSION) await conn.commit() diff --git a/tests/ingester/test_cli.py b/tests/ingester/test_cli.py index b03fd445..b9a5c8c2 100644 --- a/tests/ingester/test_cli.py +++ b/tests/ingester/test_cli.py @@ -1,8 +1,9 @@ -"""haiku-ingester run-batch CLI: echoes the batch report and exits non-zero -when any job dead-letters. IngesterApp is patched so no real ingestion runs.""" +"""haiku-ingester CLI: exercises every subcommand via CliRunner with +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 haiku.rag.ingester.app import BatchReport @@ -11,6 +12,9 @@ from haiku.rag.ingester.cli import _cli as cli runner = CliRunner() +# --- helpers --- + + def _fake_app(report: BatchReport, monkeypatch) -> AsyncMock: fake = AsyncMock() 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 "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 diff --git a/tests/ingester/test_filter.py b/tests/ingester/test_filter.py new file mode 100644 index 00000000..c880fd14 --- /dev/null +++ b/tests/ingester/test_filter.py @@ -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 diff --git a/tests/ingester/test_resolve_fetcher.py b/tests/ingester/test_resolve_fetcher.py index 7da7debb..ddf198c3 100644 --- a/tests/ingester/test_resolve_fetcher.py +++ b/tests/ingester/test_resolve_fetcher.py @@ -44,6 +44,11 @@ def test_adhoc_resolves_s3_forwards_storage_options(): 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(): with pytest.raises(ValueError, match="No source adapter"): resolve_adhoc_fetcher("ftp://example.com/x")