287 lines
9.1 KiB
Python
287 lines
9.1 KiB
Python
"""Exercise cli/llm.py's `index` and `serve` commands with mocked backends."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
from cli.llm import app
|
|
|
|
runner = CliRunner()
|
|
|
|
_STATS = {
|
|
"indexed": 3,
|
|
"skipped": 1,
|
|
"chunks": 7,
|
|
"fingerprint_skipped": 0,
|
|
"hash_skipped": 0,
|
|
"docket_complete": 0,
|
|
}
|
|
|
|
|
|
class TestIndexComments:
|
|
@patch("llm.index.docket_complete", return_value={})
|
|
@patch("llm.index._engine")
|
|
@patch("llm.source.iter_comment_refs")
|
|
@patch("llm.pool.HostPool.from_config")
|
|
@patch("llm.index.index_refs")
|
|
@patch("conf.connect.bib")
|
|
@patch("llm.config.load")
|
|
def test_default_collection_is_comments(
|
|
self,
|
|
mock_load,
|
|
mock_bib,
|
|
mock_index_refs,
|
|
mock_from_config,
|
|
mock_iter,
|
|
_engine,
|
|
_complete,
|
|
):
|
|
cfg = MagicMock()
|
|
mock_load.return_value = cfg
|
|
store = MagicMock()
|
|
store.sealed_dockets.return_value = {}
|
|
mock_bib.return_value = store
|
|
docs = iter(["doc1", "doc2"])
|
|
mock_iter.return_value = docs
|
|
pool = MagicMock()
|
|
mock_from_config.return_value = pool
|
|
mock_index_refs.return_value = _STATS
|
|
|
|
result = runner.invoke(app, ["index"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
mock_iter.assert_called_once_with(store, docket="", skip_dockets=set())
|
|
mock_index_refs.assert_called_once()
|
|
kwargs = mock_index_refs.call_args.kwargs
|
|
assert kwargs["collection"] == "comments"
|
|
assert kwargs["cfg"] is cfg
|
|
assert kwargs["pool"] is pool
|
|
assert kwargs["force"] is False
|
|
assert "indexed=3 skipped=1 chunks=7" in result.output
|
|
|
|
|
|
class TestIndexCorpus:
|
|
@patch("llm.index.docket_complete", return_value={})
|
|
@patch("llm.index._engine")
|
|
@patch("llm.source.ZoteroPdfIndex.lazy")
|
|
@patch("llm.source.iter_corpus_refs")
|
|
@patch("llm.pool.HostPool.from_config")
|
|
@patch("llm.index.index_refs")
|
|
@patch("conf.connect.bib")
|
|
@patch("llm.config.load")
|
|
def test_corpus_collection(
|
|
self,
|
|
mock_load,
|
|
mock_bib,
|
|
mock_index_refs,
|
|
mock_from_config,
|
|
mock_iter,
|
|
mock_lazy,
|
|
_engine,
|
|
_complete,
|
|
):
|
|
cfg = MagicMock()
|
|
mock_load.return_value = cfg
|
|
store = MagicMock()
|
|
store.sealed_dockets.return_value = {}
|
|
mock_bib.return_value = store
|
|
mock_iter.return_value = iter(["doc1"])
|
|
mock_from_config.return_value = MagicMock()
|
|
mock_index_refs.return_value = _STATS
|
|
zot = MagicMock()
|
|
mock_lazy.return_value = zot
|
|
|
|
result = runner.invoke(app, ["index", "--collection", "corpus"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
mock_iter.assert_called_once_with(store, zotero=zot)
|
|
kwargs = mock_index_refs.call_args.kwargs
|
|
assert kwargs["collection"] == "corpus"
|
|
assert "corpus: indexed=3 skipped=1 chunks=7" in result.output
|
|
|
|
|
|
class TestIndexAll:
|
|
@patch("llm.index.docket_complete", return_value={})
|
|
@patch("llm.index._engine")
|
|
@patch("llm.source.ZoteroPdfIndex.lazy")
|
|
@patch("llm.source.iter_corpus_refs")
|
|
@patch("llm.source.iter_rule_refs")
|
|
@patch("llm.source.iter_comment_refs")
|
|
@patch("llm.pool.HostPool.from_config")
|
|
@patch("llm.index.index_refs")
|
|
@patch("conf.connect.bib")
|
|
@patch("llm.config.load")
|
|
def test_all_runs_comments_rules_corpus_in_order(
|
|
self,
|
|
mock_load,
|
|
mock_bib,
|
|
mock_index_refs,
|
|
mock_from_config,
|
|
mock_comments,
|
|
mock_rules,
|
|
mock_corpus,
|
|
mock_lazy,
|
|
_engine,
|
|
_complete,
|
|
):
|
|
mock_load.return_value = MagicMock()
|
|
store = MagicMock()
|
|
store.sealed_dockets.return_value = {}
|
|
mock_bib.return_value = store
|
|
mock_from_config.return_value = MagicMock()
|
|
mock_index_refs.return_value = _STATS
|
|
for m in (mock_comments, mock_rules, mock_corpus):
|
|
m.return_value = iter(["d"])
|
|
|
|
result = runner.invoke(app, ["index", "--collection", "all"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert [c.kwargs["collection"] for c in mock_index_refs.call_args_list] == [
|
|
"comments",
|
|
"rules",
|
|
"corpus",
|
|
]
|
|
assert result.output.count("indexed=3") == 3
|
|
|
|
|
|
class TestIndexRules:
|
|
@patch("llm.index.docket_complete", return_value={})
|
|
@patch("llm.index._engine")
|
|
@patch("llm.source.iter_rule_refs")
|
|
@patch("llm.pool.HostPool.from_config")
|
|
@patch("llm.index.index_refs")
|
|
@patch("conf.connect.bib")
|
|
@patch("llm.config.load")
|
|
def test_rules_collection(
|
|
self,
|
|
mock_load,
|
|
mock_bib,
|
|
mock_index_refs,
|
|
mock_from_config,
|
|
mock_iter,
|
|
_engine,
|
|
_complete,
|
|
):
|
|
cfg = MagicMock()
|
|
mock_load.return_value = cfg
|
|
store = MagicMock()
|
|
store.sealed_dockets.return_value = {}
|
|
mock_bib.return_value = store
|
|
mock_iter.return_value = iter(["doc1"])
|
|
mock_from_config.return_value = MagicMock()
|
|
mock_index_refs.return_value = _STATS
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["index", "--collection", "rules", "--key", "abc123", "--key", "def456"],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
mock_iter.assert_called_once_with(store, keys=("abc123", "def456"))
|
|
kwargs = mock_index_refs.call_args.kwargs
|
|
assert kwargs["collection"] == "rules"
|
|
assert "indexed=3 skipped=1 chunks=7" in result.output
|
|
|
|
|
|
class TestIndexBadCollection:
|
|
@patch("conf.connect.bib")
|
|
@patch("llm.config.load")
|
|
def test_bad_collection_raises_bad_parameter(self, mock_load, mock_bib):
|
|
mock_load.return_value = MagicMock()
|
|
mock_bib.return_value = MagicMock()
|
|
|
|
result = runner.invoke(app, ["index", "--collection", "bogus"])
|
|
|
|
assert result.exit_code == 2
|
|
assert "collection must be comments, rules, corpus or all" in result.output
|
|
|
|
|
|
class TestIndexLimit:
|
|
@patch("llm.index.docket_complete", return_value={})
|
|
@patch("llm.index._engine")
|
|
@patch("llm.source.iter_comment_refs")
|
|
@patch("llm.pool.HostPool.from_config")
|
|
@patch("llm.index.index_refs")
|
|
@patch("conf.connect.bib")
|
|
@patch("llm.config.load")
|
|
def test_limit_truncates_docs(
|
|
self,
|
|
mock_load,
|
|
mock_bib,
|
|
mock_index_refs,
|
|
mock_from_config,
|
|
mock_iter,
|
|
_engine,
|
|
_complete,
|
|
):
|
|
mock_load.return_value = MagicMock()
|
|
store = MagicMock()
|
|
store.sealed_dockets.return_value = {}
|
|
mock_bib.return_value = store
|
|
mock_iter.return_value = iter([f"doc{i}" for i in range(5)])
|
|
mock_from_config.return_value = MagicMock()
|
|
mock_index_refs.return_value = _STATS
|
|
|
|
result = runner.invoke(app, ["index", "--limit", "2"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
refs_arg = mock_index_refs.call_args.args[0]
|
|
assert list(refs_arg) == ["doc0", "doc1"]
|
|
assert mock_index_refs.call_args.kwargs["mark_complete"] is False
|
|
|
|
|
|
class TestServe:
|
|
@patch("uvicorn.run")
|
|
def test_serve_starts_uvicorn(self, mock_run):
|
|
result = runner.invoke(app, ["serve", "--host", "0.0.0.0", "--port", "9000"])
|
|
assert result.exit_code == 0
|
|
mock_run.assert_called_once_with(
|
|
"llm.api:app", host="0.0.0.0", port=9000, log_level="info"
|
|
)
|
|
|
|
|
|
class TestHosts:
|
|
@patch("llm.pool.pick_model", return_value="qwen2.5:32b")
|
|
@patch("llm.pool.HostPool.status")
|
|
@patch("llm.pool.HostPool.check")
|
|
@patch("llm.config.load")
|
|
def test_lists_fleet_and_generation_pick(
|
|
self, mock_load, mock_check, mock_status, _pm
|
|
):
|
|
from llm.config import LlmConfig
|
|
|
|
rack, rig = "http://ollama:11434", "http://rig:11434"
|
|
mock_load.return_value = LlmConfig(
|
|
ollama_hosts=(rack, rig),
|
|
host_vram={rack: 12, rig: 24},
|
|
embed_model="e",
|
|
instruct_model="qwen2.5:14b",
|
|
embed_dim=768,
|
|
build_ann_index=False,
|
|
pg_host="x",
|
|
pg_port=5432,
|
|
pg_db="llm",
|
|
pg_user="llm",
|
|
)
|
|
mock_check.return_value = [rig]
|
|
mock_status.return_value = [
|
|
{"host": rig, "vram_gb": 24.0, "models": ["qwen2.5:32b"]}
|
|
]
|
|
|
|
result = runner.invoke(app, ["hosts"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "http://rig:11434" in result.output and "24 GB" in result.output
|
|
assert "http://ollama:11434" in result.output and "DOWN" in result.output
|
|
assert "generation" in result.output and "qwen2.5:32b @" in result.output
|
|
|
|
@patch("llm.pool.HostPool.check", side_effect=RuntimeError("no Ollama host"))
|
|
@patch("llm.config.load")
|
|
def test_no_live_hosts_exits_1(self, mock_load, _check):
|
|
mock_load.return_value = MagicMock(ollama_hosts=("http://h:1",), host_vram={})
|
|
result = runner.invoke(app, ["hosts"])
|
|
assert result.exit_code == 1
|
|
assert "no Ollama host" in result.output
|