Improve onboarding CLI resilience and diagnostics (#45)

* Improve onboarding CLI resilience and diagnostics

* chore: update version to 1.0.0
This commit is contained in:
Abhay
2026-03-13 12:25:06 -07:00
committed by GitHub
parent 8de28bdf1e
commit 4b29ce5372
8 changed files with 151 additions and 18 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "openjarvis-chat",
"private": true,
"version": "0.1.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
+13 -7
View File
@@ -176,9 +176,9 @@ def _check_default_model() -> CheckResult:
if not default_model:
return CheckResult(
"Default model",
"warn",
"Not configured",
details="Set intelligence.default_model in config.toml.",
"ok",
"Not configured (auto-routing enabled)",
details="Router will select a model dynamically.",
)
_ensure_engines_imported()
@@ -213,7 +213,7 @@ def _check_optional_deps() -> List[CheckResult]:
optional_packages = [
("fastapi", "openjarvis[server]", "REST API server"),
("torch", "pip install torch", "SFT/GRPO training"),
("pynvml", "openjarvis[energy-nvidia]", "NVIDIA energy monitoring"),
("pynvml", "openjarvis[gpu-metrics]", "NVIDIA energy monitoring"),
("amdsmi", "openjarvis[energy-amd]", "AMD energy monitoring"),
("colbert", "openjarvis[memory-colbert]", "ColBERT memory backend"),
("zeus", "openjarvis[energy-apple]", "Apple Silicon energy monitoring"),
@@ -236,14 +236,17 @@ def _check_optional_deps() -> List[CheckResult]:
def _check_nodejs() -> CheckResult:
"""Check Node.js version (>= 22 required for OpenClaw)."""
"""Check Node.js version for Node-backed integrations."""
node_path = shutil.which("node")
if not node_path:
return CheckResult(
"Node.js",
"warn",
"Not found",
details="Node.js 22+ is required for OpenClaw agent.",
details=(
"Node.js 22+ is required for ClaudeCodeAgent and the "
"WhatsApp Baileys channel bridge."
),
)
try:
result = subprocess.run(
@@ -262,7 +265,10 @@ def _check_nodejs() -> CheckResult:
"Node.js",
"warn",
f"{version_str} (requires >= v22)",
details="Upgrade Node.js for OpenClaw agent support.",
details=(
"Upgrade Node.js for ClaudeCodeAgent and WhatsApp "
"Baileys support."
),
)
except Exception as exc:
return CheckResult("Node.js", "warn", f"Error checking version: {exc}")
+6 -2
View File
@@ -7,6 +7,7 @@ from typing import Optional
import click
from rich.console import Console
from rich.markup import escape
from rich.panel import Panel
from openjarvis.core.config import (
@@ -161,7 +162,11 @@ def init(force: bool, config: Optional[Path], full_config: bool = False) -> None
console.print()
console.print(
Panel(toml_content, title=str(DEFAULT_CONFIG_PATH), border_style="green")
Panel(
escape(toml_content),
title=str(DEFAULT_CONFIG_PATH),
border_style="green",
)
)
console.print("[green]Config written successfully.[/green]")
@@ -177,4 +182,3 @@ def init(force: bool, config: Optional[Path], full_config: bool = False) -> None
border_style="cyan",
)
)
+34 -8
View File
@@ -36,6 +36,20 @@ def _check_engine_health(engine_key: str) -> bool:
return False
def _discover_healthy_engines() -> list[str]:
"""Return keys for all healthy engines discovered at runtime."""
try:
import openjarvis.engine # noqa: F401 — trigger registration
from openjarvis.core.config import load_config
from openjarvis.engine import _discovery
config = load_config()
return [key for key, _ in _discovery.discover_engines(config)]
except Exception as exc:
logger.warning("Healthy engine discovery failed: %s", exc)
return []
def _check_model_available(engine_key: str) -> bool:
"""Return True if at least one model is available on the engine."""
try:
@@ -104,18 +118,30 @@ def quickstart(force: bool) -> None:
# Step 3: Check engine
console.print()
console.print(f"[bold cyan][3/5][/bold cyan] Checking engine ({engine_key})...")
active_engine = engine_key
if not _check_engine_health(engine_key):
console.print(f" [red bold]Engine '{engine_key}' is not reachable.[/red bold]")
console.print()
console.print(f" Start the {engine_key} server and try again.")
console.print(" Run [bold]jarvis doctor[/bold] for detailed diagnostics.")
raise SystemExit(1)
console.print(f" [green]Engine '{engine_key}' is healthy.[/green]")
fallbacks = [k for k in _discover_healthy_engines() if k != engine_key]
if fallbacks:
active_engine = fallbacks[0]
console.print(
f" [yellow]Engine '{engine_key}' is not reachable; "
f"falling back to '{active_engine}'.[/yellow]"
)
else:
console.print(
f" [red bold]Engine '{engine_key}' is not reachable.[/red bold]"
)
console.print()
console.print(f" Start the {engine_key} server and try again.")
console.print(" Run [bold]jarvis doctor[/bold] for detailed diagnostics.")
raise SystemExit(1)
else:
console.print(f" [green]Engine '{engine_key}' is healthy.[/green]")
# Step 4: Verify model
console.print()
console.print("[bold cyan][4/5][/bold cyan] Checking for available models...")
if not _check_model_available(engine_key):
if not _check_model_available(active_engine):
console.print(" [yellow]No models found.[/yellow]")
console.print(
" Pull a model first (e.g. [bold]ollama pull qwen3.5:3b[/bold])."
@@ -126,7 +152,7 @@ def quickstart(force: bool) -> None:
# Step 5: Test query
console.print()
console.print("[bold cyan][5/5][/bold cyan] Running test query...")
response = _test_query(engine_key)
response = _test_query(active_engine)
console.print(f" [green]Response:[/green] {response[:200]}")
console.print()
+14
View File
@@ -11,6 +11,7 @@ from click.testing import CliRunner
from openjarvis.cli import cli
from openjarvis.cli.doctor_cmd import (
_check_config_exists,
_check_default_model,
_check_nodejs,
_check_python_version,
)
@@ -146,6 +147,17 @@ class TestCheckEngineProbing:
assert vllm_result.status == "warn"
class TestCheckDefaultModel:
def test_empty_default_model_is_not_a_warning(self) -> None:
"""Leaving default model empty should be treated as valid auto-routing."""
mock_config = MagicMock()
mock_config.intelligence.default_model = ""
with patch("openjarvis.cli.doctor_cmd.load_config", return_value=mock_config):
result = _check_default_model()
assert result.status == "ok"
assert "auto" in result.message.lower()
class TestCheckNodejs:
def test_check_nodejs_found(self) -> None:
"""Node.js check reports version when node is available."""
@@ -166,3 +178,5 @@ class TestCheckNodejs:
result = _check_nodejs()
assert result.status == "warn"
assert "Not found" in result.message
assert result.details is not None
assert "OpenClaw" not in result.details
+14
View File
@@ -50,3 +50,17 @@ class TestDoctorOptionalLabels:
]
assert len(apple_checks) == 1
assert "Not installed (openjarvis[energy-apple])" == apple_checks[0]["message"]
def test_nvidia_label_uses_current_extra_name(self) -> None:
"""NVIDIA hint should reference the current project extra."""
blocker = _selective_import_blocker("pynvml")
with mock.patch("builtins.__import__", side_effect=blocker):
runner = CliRunner()
result = runner.invoke(cli, ["doctor", "--json"])
data = json.loads(result.output)
nvidia_checks = [
c for c in data
if c["name"] == "Optional: NVIDIA energy monitoring"
]
assert len(nvidia_checks) == 1
assert "Not installed (openjarvis[gpu-metrics])" == nvidia_checks[0]["message"]
+17
View File
@@ -30,6 +30,23 @@ class TestInitShowsNextSteps:
assert "jarvis ask" in result.output
assert "jarvis doctor" in result.output
def test_init_output_shows_toml_sections_literally(self, tmp_path: Path) -> None:
"""Init output should render TOML section headers like [engine] literally."""
config_dir = tmp_path / ".openjarvis"
config_path = config_dir / "config.toml"
with (
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir
),
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path
),
):
result = CliRunner().invoke(cli, ["init"])
assert result.exit_code == 0
assert "[engine]" in result.output
assert "[intelligence]" in result.output
class TestNextStepsOllama:
def test_next_steps_ollama(self) -> None:
+52
View File
@@ -191,3 +191,55 @@ class TestQuickstartCommand:
"engine" in result.output.lower()
or "not reachable" in result.output.lower()
)
def test_falls_back_to_any_healthy_engine(self, tmp_path):
"""If the recommended engine is down, use a healthy fallback engine."""
config_path = tmp_path / "config.toml"
hw = MagicMock()
hw.platform = "darwin"
hw.cpu_brand = "Apple M1"
hw.cpu_count = 8
hw.ram_gb = 16
hw.gpu = MagicMock(name="Apple GPU", vram_gb=16, count=1, vendor="apple")
with (
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch(
"openjarvis.cli.quickstart_cmd"
".generate_default_toml",
return_value="[engine]\ndefault = \"mlx\"\n",
),
patch(
"openjarvis.cli.quickstart_cmd"
".recommend_engine",
return_value="mlx",
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_engine_health",
return_value=False,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._discover_healthy_engines",
return_value=["ollama"],
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_model_available",
return_value=True,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._test_query",
return_value="Hello!",
),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart"])
assert result.exit_code == 0
assert "falling back" in result.output.lower()
assert "ollama" in result.output