Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/modeldock/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from modeldock.cli.commands.search import search_cmd
from modeldock.cli.commands.sources import sources_app
from modeldock.cli.commands.update import update_cmd
from modeldock.cli.commands.verify import verify_cmd
from modeldock.common.logging import configure_logging

# Allowed log levels surfaced to the user via --help and validated in the CLI
Expand All @@ -46,6 +47,7 @@
app.command("search")(search_cmd)
app.command("info")(info_cmd)
app.command("update")(update_cmd)
app.command("verify")(verify_cmd)
app.command("remove")(remove_cmd)
app.command("run")(run_cmd)
app.command("runtimes")(runtimes_cmd)
Expand Down
44 changes: 44 additions & 0 deletions src/modeldock/cli/commands/verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""CLI command: verify."""

from __future__ import annotations

import typer

from modeldock.cli.console import print_error, render_json, render_verify
from modeldock.cli.factory import manager_for


def verify_cmd(
models: list[str] = typer.Argument(None, help="Model name(s) to verify"),
all_: bool = typer.Option(False, "--all", help="Verify every installed model"),
backend: str = typer.Option(None, "--backend", help="Runtime backend"),
json_out: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
debug: bool = typer.Option(False, "--debug", help="Show traceback"),
) -> None:
"""Verify installed model(s) integrity."""
try:
mgr = manager_for(backend)

if all_:
if models:
raise typer.BadParameter("Cannot pass model names together with --all")
names = [ref.name for ref in mgr.installed()]
else:
if not models:
raise typer.BadParameter("Provide model name(s) or use --all")
names = models

results = [(name, mgr.verify(name)) for name in names]

if json_out:
render_json([{"name": name, "ok": ok} for name, ok in results])
else:
render_verify(results)

if not all(ok for _, ok in results):
raise typer.Exit(code=1)
except typer.Exit:
raise
except Exception as exc: # noqa: BLE001 - top-level CLI boundary
print_error(exc, debug, as_json=json_out)
raise typer.Exit(code=1) # noqa: B904
15 changes: 15 additions & 0 deletions src/modeldock/cli/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,27 @@ def render_sources(sources: List[Any]) -> None:
console.print(table)


def render_verify(results: List[Any]) -> None:
"""Render verify results (name, ok) pairs as a rich table."""
from rich.console import Console
from rich.table import Table

console = Console()
table = Table(title="Verify Results")
table.add_column("Name")
table.add_column("Status")
for name, ok in results:
table.add_row(name, "[green]OK[/]" if ok else "[red]FAILED[/]")
console.print(table)


__all__ = [
"print_error",
"render_installed",
"render_json",
"render_models",
"render_runtimes",
"render_sources",
"render_verify",
"to_jsonable",
]
109 changes: 109 additions & 0 deletions tests/unit/test_verify_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Unit tests: verify command — exit codes, --all behavior, JSON output."""

from __future__ import annotations

from typing import Any, Optional

import pytest
from typer.testing import CliRunner

import modeldock.cli.factory as factory
from modeldock.cli.app import app
from modeldock.domain.model import ModelRef

runner = CliRunner()


class _VerifyManager:
"""Stub manager: installed() returns configured refs, verify() looks up results."""

def __init__(
self,
installed_refs: Optional[list[ModelRef]] = None,
verify_results: Optional[dict[str, bool]] = None,
**_: Any,
) -> None:
self._installed_refs = installed_refs or []
self._verify_results = verify_results or {}

def installed(self) -> list[ModelRef]:
return self._installed_refs

def verify(self, name: str) -> bool:
return self._verify_results.get(name, True)


# ---------------------------------------------------------------------------
# --all
# ---------------------------------------------------------------------------


def test_verify_all_all_pass_exits_0(monkeypatch: pytest.MonkeyPatch) -> None:
refs = [ModelRef.parse("llama3"), ModelRef.parse("qwen3")]
mgr = _VerifyManager(installed_refs=refs, verify_results={"llama3": True, "qwen3": True})
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify", "--all"])
assert result.exit_code == 0


def test_verify_all_one_fails_exits_1(monkeypatch: pytest.MonkeyPatch) -> None:
refs = [ModelRef.parse("llama3"), ModelRef.parse("qwen3")]
mgr = _VerifyManager(installed_refs=refs, verify_results={"llama3": True, "qwen3": False})
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify", "--all"])
assert result.exit_code == 1


def test_verify_all_empty_installed_exits_0(monkeypatch: pytest.MonkeyPatch) -> None:
mgr = _VerifyManager(installed_refs=[])
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify", "--all"])
assert result.exit_code == 0


def test_verify_all_with_explicit_models_exits_1(monkeypatch: pytest.MonkeyPatch) -> None:
"""--all combined with explicit model names is invalid, not silently merged."""
mgr = _VerifyManager()
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify", "--all", "llama3"])
assert result.exit_code == 1


# ---------------------------------------------------------------------------
# explicit model names
# ---------------------------------------------------------------------------


def test_verify_explicit_model_passes_exits_0(monkeypatch: pytest.MonkeyPatch) -> None:
mgr = _VerifyManager(verify_results={"llama3": True})
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify", "llama3"])
assert result.exit_code == 0


def test_verify_explicit_model_fails_exits_1(monkeypatch: pytest.MonkeyPatch) -> None:
mgr = _VerifyManager(verify_results={"llama3": False})
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify", "llama3"])
assert result.exit_code == 1


def test_verify_no_args_no_all_exits_1(monkeypatch: pytest.MonkeyPatch) -> None:
mgr = _VerifyManager()
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify"])
assert result.exit_code == 1


# ---------------------------------------------------------------------------
# --json
# ---------------------------------------------------------------------------


def test_verify_json_output_shape(monkeypatch: pytest.MonkeyPatch) -> None:
mgr = _VerifyManager(verify_results={"llama3": True})
monkeypatch.setattr(factory, "ModelManager", lambda **kw: mgr)
result = runner.invoke(app, ["verify", "llama3", "--json"])
assert result.exit_code == 0
assert '"name": "llama3"' in result.stdout
assert '"ok": true' in result.stdout