feat: add CLI commands: config, registry, tool (#98)

* feat: add CLI commands: config, registry, tool

* fix: address review feedback on CLI config/registry/tool commands

- Replace non-existent `create_config_template` with `generate_default_toml`
  to fix ImportError when config file is missing
- Deduplicate registry maps into shared `_load_registry_map()` helper
- Remove dead `_get_registry_class()` function and its tests
- Route JSON output to stdout for pipeability (`jarvis config show json | jq`)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
geodab
2026-03-23 01:38:23 -07:00
committed by GitHub
co-authored by Claude Opus 4.6 Jon Saad-Falcon
parent 8468f08c41
commit 04c28ef7ed
7 changed files with 1138 additions and 0 deletions
+6
View File
@@ -12,6 +12,7 @@ from openjarvis.cli.bench_cmd import bench
from openjarvis.cli.channel_cmd import channel
from openjarvis.cli.chat_cmd import chat
from openjarvis.cli.compose_cmd import compose
from openjarvis.cli.config_cmd import config
from openjarvis.cli.daemon_cmd import restart, start, status, stop
from openjarvis.cli.doctor_cmd import doctor
from openjarvis.cli.eval_cmd import eval_group
@@ -24,10 +25,12 @@ from openjarvis.cli.model import model
from openjarvis.cli.operators_cmd import operators
from openjarvis.cli.optimize_cmd import optimize_group
from openjarvis.cli.quickstart_cmd import quickstart
from openjarvis.cli.registry_cmd import registry
from openjarvis.cli.scheduler_cmd import scheduler
from openjarvis.cli.serve import serve
from openjarvis.cli.skill_cmd import skill
from openjarvis.cli.telemetry_cmd import telemetry
from openjarvis.cli.tool_cmd import tool
from openjarvis.cli.vault_cmd import vault
from openjarvis.cli.workflow_cmd import workflow
@@ -81,6 +84,9 @@ cli.add_command(optimize_group, "optimize")
cli.add_command(feedback_group, "feedback")
cli.add_command(compose, "compose")
cli.add_command(gateway, "gateway")
cli.add_command(tool, "tool")
cli.add_command(registry, "registry")
cli.add_command(config, "config")
def main() -> None:
+277
View File
@@ -0,0 +1,277 @@
"""``jarvis config`` — configuration inspection commands."""
from __future__ import annotations
import json
import os
from pathlib import Path
import click
from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
@click.group()
def config() -> None:
"""Inspect configuration — show loaded settings, hardware, and config files."""
def _get_config_path(path: str | None) -> Path:
"""Determine the config path from argument or environment."""
from openjarvis.core.config import DEFAULT_CONFIG_PATH
if path:
return Path(path)
return Path(os.environ.get("OPENJARVIS_CONFIG", DEFAULT_CONFIG_PATH))
def _show_hardware_info(console: Console, show_recommendations: bool = True) -> None:
"""Display detected hardware information."""
from openjarvis.core.config import detect_hardware, recommend_engine
hardware = detect_hardware()
console.print("\n[bold]Detected Hardware[/bold]")
hardware_table = Table(show_header=True, header_style="cyan")
hardware_table.add_column("Component", style="green")
hardware_table.add_column("Value", style="white")
# Platform
hardware_table.add_row("Platform", str(hardware.platform))
# CPU
if hardware.cpu_brand:
hardware_table.add_row("CPU", hardware.cpu_brand)
hardware_table.add_row("CPU Count", str(hardware.cpu_count))
hardware_table.add_row("RAM", f"{hardware.ram_gb:.1f} GB")
# GPU
if hardware.gpu:
hardware_table.add_row("GPU Vendor", hardware.gpu.vendor)
hardware_table.add_row("GPU Model", hardware.gpu.name)
hardware_table.add_row("GPU VRAM", f"{hardware.gpu.vram_gb:.1f} GB")
hardware_table.add_row("GPU Count", str(hardware.gpu.count))
console.print(hardware_table)
# Show recommended engine
if show_recommendations:
recommended = recommend_engine(hardware)
console.print(f"\n[bold]Recommended Engine:[/bold] [cyan]{recommended}[/cyan]")
def _show_config_template(console: Console, config_path: Path) -> None:
"""Show default config template when config file doesn't exist."""
from openjarvis.core.config import (
DEFAULT_CONFIG_DIR,
detect_hardware,
generate_default_toml,
)
console.print(f"[yellow]Config file not found: {config_path}[/yellow]")
config_location = str(DEFAULT_CONFIG_DIR / "config.toml")
msg = f"Create one at {config_location} or use --path to specify a location."
console.print(f"[dim]{msg}[/dim]")
console.print("\n[bold]Default Configuration Template:[/bold]")
hw = detect_hardware()
template = generate_default_toml(hw)
syntax = Syntax(template, "toml", theme="monokai", line_numbers=True)
console.print(Panel(syntax, border_style="dim"))
def _show_loaded_config(console: Console, config_path: Path, as_json: bool) -> None:
"""Show the loaded effective configuration from config.toml."""
from openjarvis.core.config import load_config
console.print(f"[dim]Loading config from: {config_path}[/dim]")
if config_path.exists():
config = load_config(config_path)
if as_json:
# Convert the dataclass to a dict and output as JSON
from dataclasses import fields, is_dataclass
def convert(obj):
if obj is None:
return None
if isinstance(obj, (str, int, float, bool)):
return obj
if isinstance(obj, (list, tuple)):
return [convert(item) for item in obj]
if isinstance(obj, dict):
return {k: convert(v) for k, v in obj.items()}
if is_dataclass(obj):
result = {}
for field in fields(obj):
field_value = getattr(obj, field.name)
if field_value is not None:
result[field.name] = convert(field_value)
return result
# Fallback for other types
return str(obj)
config_dict = convert(config)
# Write JSON to stdout so it is pipeable
stdout_console = Console()
stdout_console.print_json(json.dumps(config_dict, indent=2, default=str))
else:
# Show as formatted table
console.print("[bold]Engine Configuration[/bold]")
console.print(f" Default Engine: [cyan]{config.engine.default}[/cyan]")
console.print("\n[bold]Intelligence Configuration[/bold]")
console.print(
f" Default Model: [cyan]{config.intelligence.default_model}[/cyan]"
)
fallback = config.intelligence.fallback_model or "N/A"
console.print(f" Fallback Model: [cyan]{fallback}[/cyan]")
console.print(
f" Temperature: [cyan]{config.intelligence.temperature}[/cyan]"
)
console.print(
f" Max Tokens: [cyan]{config.intelligence.max_tokens}[/cyan]"
)
console.print("\n[bold]Agent Configuration[/bold]")
console.print(f" Default Agent: [cyan]{config.agent.default_agent}[/cyan]")
console.print(f" Max Turns: [cyan]{config.agent.max_turns}[/cyan]")
console.print(f" Tools: [cyan]{config.agent.tools or 'none'}[/cyan]")
ctx_mem = config.agent.context_from_memory
console.print(f" Context from Memory: [cyan]{ctx_mem}[/cyan]")
# Show hardware info
_show_hardware_info(console)
else:
_show_config_template(console, config_path)
def _show_toml_config(console: Console, config_path: Path) -> None:
"""Show the raw TOML configuration file content with syntax highlighting."""
console.print(f"[dim]Loading config from: {config_path}[/dim]")
if config_path.exists():
config_content = config_path.read_text()
syntax = Syntax(config_content, "toml", theme="monokai", line_numbers=True)
console.print(Panel(syntax, title="Config File", border_style="cyan"))
else:
_show_config_template(console, config_path)
def _show_json_config(console: Console, config_path: Path) -> None:
"""Show the parsed TOML configuration as JSON."""
console.print(f"[dim]Loading config from: {config_path}[/dim]")
if config_path.exists():
config_content = config_path.read_text()
try:
import tomllib # Python 3.11+
except ModuleNotFoundError:
import tomli as tomllib # type: ignore[no-redef]
config_dict = tomllib.loads(config_content)
# Write JSON to stdout so it is pipeable
stdout_console = Console()
stdout_console.print_json(json.dumps(config_dict, indent=2))
else:
_show_config_template(console, config_path)
def _show_hardware(console: Console) -> None:
"""Show detected hardware information with recommended engine and model."""
from openjarvis.core.config import (
detect_hardware,
recommend_engine,
recommend_model,
)
hardware = detect_hardware()
_show_hardware_info(console, show_recommendations=False)
# Show recommended engine
recommended_engine = recommend_engine(hardware)
console.print(
f"\n[bold]Recommended Engine:[/bold] [cyan]{recommended_engine}[/cyan]"
)
# Show recommended model
console.print("\n[bold]Model Recommendations[/bold]")
recommended_model = recommend_model(hardware, recommended_engine)
console.print(f" Recommended Model: [cyan]{recommended_model}[/cyan]")
# Nested group for show sub-commands
@click.group(invoke_without_command=True)
@click.option("--path", "-p", default=None, help="Explicit config file path")
@click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON")
@click.pass_context
def show_group(ctx: click.Context, path: str | None, as_json: bool) -> None:
"""Show configuration details."""
# Default to 'loaded' if no subcommand is invoked
if ctx.invoked_subcommand is None:
console = Console(stderr=True)
try:
config_path = _get_config_path(path)
_show_loaded_config(console, config_path, as_json)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
@show_group.command()
@click.option("--path", "-p", default=None, help="Explicit config file path")
@click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON")
def loaded(path: str | None, as_json: bool) -> None:
"""Show the loaded effective configuration from config.toml."""
console = Console(stderr=True)
try:
config_path = _get_config_path(path)
_show_loaded_config(console, config_path, as_json)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
@show_group.command()
@click.option("--path", "-p", default=None, help="Explicit config file path")
def toml(path: str | None) -> None:
"""Show the raw TOML configuration file content with syntax highlighting."""
console = Console(stderr=True)
try:
config_path = _get_config_path(path)
_show_toml_config(console, config_path)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
@show_group.command("json")
@click.option("--path", "-p", default=None, help="Explicit config file path")
def as_json(path: str | None) -> None:
"""Show the parsed TOML configuration as JSON."""
console = Console(stderr=True)
try:
config_path = _get_config_path(path)
_show_json_config(console, config_path)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
@show_group.command()
def hardware() -> None:
"""Show detected hardware information with recommended engine and model."""
console = Console(stderr=True)
try:
_show_hardware(console)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
# Register the show group under config
config.add_command(show_group, "show")
__all__ = ["config"]
+146
View File
@@ -0,0 +1,146 @@
"""``jarvis registry`` — registry inspection commands."""
from __future__ import annotations
import click
from rich.console import Console
from rich.table import Table
def _load_registry_map() -> tuple[dict[str, object], dict[str, object]]:
"""Import all registries and return (by_name, aliases) lookup dicts."""
from openjarvis.core.registry import (
AgentRegistry,
BenchmarkRegistry,
ChannelRegistry,
CompressionRegistry,
EngineRegistry,
LearningRegistry,
MemoryRegistry,
ModelRegistry,
RouterPolicyRegistry,
SkillRegistry,
SpeechRegistry,
ToolRegistry,
)
by_name = {
"ToolRegistry": ToolRegistry,
"AgentRegistry": AgentRegistry,
"EngineRegistry": EngineRegistry,
"MemoryRegistry": MemoryRegistry,
"ModelRegistry": ModelRegistry,
"ChannelRegistry": ChannelRegistry,
"LearningRegistry": LearningRegistry,
"SkillRegistry": SkillRegistry,
"BenchmarkRegistry": BenchmarkRegistry,
"RouterPolicyRegistry": RouterPolicyRegistry,
"SpeechRegistry": SpeechRegistry,
"CompressionRegistry": CompressionRegistry,
}
aliases: dict[str, object] = {}
_alias_map = {
"ToolRegistry": ("tool", "tools"),
"AgentRegistry": ("agent", "agents"),
"EngineRegistry": ("engine", "engines"),
"MemoryRegistry": ("memory", "memories"),
"ModelRegistry": ("model", "models"),
"ChannelRegistry": ("channel", "channels"),
"LearningRegistry": ("learning", "learnings"),
"SkillRegistry": ("skill", "skills"),
"BenchmarkRegistry": ("benchmark", "benchmarks"),
"RouterPolicyRegistry": ("router", "routers"),
"SpeechRegistry": ("speech", "speeches"),
"CompressionRegistry": ("compression", "compressions"),
}
for class_name, cls in by_name.items():
aliases[class_name] = cls
for alias in _alias_map[class_name]:
aliases[alias] = cls
return by_name, aliases
@click.group()
def registry() -> None:
"""Inspect registered components — list registries, show entries."""
@registry.command("list")
def list_registries() -> None:
"""List all available registries."""
console = Console(stderr=True)
table = Table(title="Available Registries")
table.add_column("Registry", style="cyan")
table.add_column("Module", style="green")
table.add_column("Entry Count", style="yellow")
try:
by_name, _ = _load_registry_map()
except Exception as exc:
console.print(f"[red]Error loading registries: {exc}[/red]")
return
module_path = "openjarvis.core.registry"
for reg_name, registry_cls in by_name.items():
try:
count = len(registry_cls.keys())
table.add_row(reg_name, module_path, str(count))
except Exception as exc:
table.add_row(reg_name, module_path, f"[red]Error: {exc}[/red]")
console.print(table)
@registry.command()
@click.argument("registry_name")
@click.option(
"--verbose", "-v", is_flag=True, default=False, help="Show full entry details"
)
def show(registry_name: str, verbose: bool) -> None:
"""Show entries in a specific registry."""
console = Console(stderr=True)
try:
_, aliases = _load_registry_map()
registry_cls = aliases.get(registry_name)
if registry_cls is None:
console.print(f"[red]Unknown registry: {registry_name}[/red]")
console.print(
"[dim]Run 'jarvis registry list' to see available registries.[/dim]"
)
return
keys = registry_cls.keys()
if not keys:
console.print(f"[dim]{registry_name} is empty.[/dim]")
return
console.print(f"[bold]{registry_name}[/bold] — {len(keys)} entry/entries")
if verbose:
for key in keys:
entry = registry_cls.get(key)
console.print(f"\n [cyan]{key}[/cyan]")
console.print(f" Type: {type(entry).__name__}")
console.print(f" Value: {entry}")
else:
table = Table()
table.add_column("Key", style="cyan")
table.add_column("Type", style="green")
table.add_column("Value", style="white", max_width=80)
for key in keys:
entry = registry_cls.get(key)
entry_type = type(entry).__name__
entry_value = str(entry)
table.add_row(key, entry_type, entry_value)
console.print(table)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
__all__ = ["registry"]
+107
View File
@@ -0,0 +1,107 @@
"""``jarvis tool`` — tool management commands."""
from __future__ import annotations
import click
from rich.console import Console
from rich.table import Table
@click.group()
def tool() -> None:
"""Manage tools — list, inspect."""
@tool.command("list")
def list_tools() -> None:
"""List all registered tools with their descriptions."""
console = Console(stderr=True)
try:
# Trigger tool registration by importing the tools module
import openjarvis.tools # noqa: F401
from openjarvis.core.registry import ToolRegistry
keys = sorted(ToolRegistry.keys())
if not keys:
console.print("[dim]No tools registered.[/dim]")
return
table = Table(title="Registered Tools")
table.add_column("Name", style="cyan")
table.add_column("Description", style="green", max_width=60)
table.add_column("Category", style="yellow")
for key in keys:
tool_cls = ToolRegistry.get(key)
description = ""
category = ""
# Try to get spec from the class or an instance
try:
# Some tools may require initialization, so try with default init
tool_instance = tool_cls() if callable(tool_cls) else tool_cls
if hasattr(tool_instance, "spec"):
spec = tool_instance.spec
description = getattr(spec, "description", "")[:60]
category = getattr(spec, "category", "")
except Exception:
# If instantiation fails, just show the key
description = "N/A (instantiation error)"
table.add_row(key, description, category)
console.print(table)
console.print(f"\n[dim]Total: {len(keys)} tool(s)[/dim]")
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
@tool.command()
@click.argument("tool_name")
def inspect(tool_name: str) -> None:
"""Show detailed information about a specific tool."""
console = Console(stderr=True)
try:
# Trigger tool registration by importing the tools module
import openjarvis.tools # noqa: F401
from openjarvis.core.registry import ToolRegistry
if not ToolRegistry.contains(tool_name):
console.print(f"[red]Tool not found: {tool_name}[/red]")
console.print("[dim]Run 'jarvis tool list' to see available tools.[/dim]")
return
tool_cls = ToolRegistry.get(tool_name)
console.print(f"[bold]{tool_name}[/bold]")
try:
tool_instance = tool_cls() if callable(tool_cls) else tool_cls
if hasattr(tool_instance, "spec"):
spec = tool_instance.spec
console.print(f" [cyan]Name:[/cyan] {getattr(spec, 'name', 'N/A')}")
console.print(
f" [cyan]Description:[/cyan] {getattr(spec, 'description', 'N/A')}"
)
category = getattr(spec, "category", "N/A") or "none"
console.print(f" [cyan]Category:[/cyan] {category}")
params = getattr(spec, "parameters", {})
if params:
console.print(" [cyan]Parameters:[/cyan]")
if isinstance(params, dict) and "properties" in params:
for param_name, param_info in params["properties"].items():
param_type = param_info.get("type", "any")
param_desc = param_info.get("description", "")
console.print(
f"{param_name}: {param_type}{param_desc}"
)
else:
console.print(f" {params}")
except Exception as e:
console.print(f"[yellow]Note: Could not instantiate tool: {e}[/yellow]")
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
__all__ = ["tool"]
+210
View File
@@ -0,0 +1,210 @@
"""Tests for the ``jarvis config`` CLI commands."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from click.testing import CliRunner
from openjarvis.cli import cli
class TestConfigCmd:
"""Test cases for the jarvis config CLI group."""
def test_config_group_help(self) -> None:
"""Test that the config group help displays correctly."""
result = CliRunner().invoke(cli, ["config", "--help"])
assert result.exit_code == 0
assert "config" in result.output.lower()
def test_config_show_help(self) -> None:
"""Test that the config show help displays correctly."""
result = CliRunner().invoke(cli, ["config", "show", "--help"])
assert result.exit_code == 0
assert "show" in result.output.lower()
def test_config_show_loaded_help(self) -> None:
"""Test that the config show loaded help displays correctly."""
result = CliRunner().invoke(cli, ["config", "show", "loaded", "--help"])
assert result.exit_code == 0
assert "loaded" in result.output.lower()
def test_config_show_toml_help(self) -> None:
"""Test that the config show toml help displays correctly."""
result = CliRunner().invoke(cli, ["config", "show", "toml", "--help"])
assert result.exit_code == 0
assert "toml" in result.output.lower()
def test_config_show_json_help(self) -> None:
"""Test that the config show json help displays correctly."""
result = CliRunner().invoke(cli, ["config", "show", "json", "--help"])
assert result.exit_code == 0
assert "json" in result.output.lower()
def test_config_show_hardware_help(self) -> None:
"""Test that the config show hardware help displays correctly."""
result = CliRunner().invoke(cli, ["config", "show", "hardware", "--help"])
assert result.exit_code == 0
assert "hardware" in result.output.lower()
def test_config_show_loaded_displays_config(self, tmp_path: Path) -> None:
"""Test that config show loaded displays the configuration."""
# Create a temporary config file
config_file = tmp_path / "test_config.toml"
config_file.write_text(
"""
[engine]
default = "ollama"
[intelligence]
default_model = "test-model"
temperature = 0.7
max_tokens = 1024
[agent]
default_agent = "simple"
max_turns = 5
"""
)
result = CliRunner().invoke(
cli, ["config", "show", "loaded", "--path", str(config_file)]
)
assert result.exit_code == 0
assert "ollama" in result.output
assert "test-model" in result.output
def test_config_show_loaded_json_output(self, tmp_path: Path) -> None:
"""Test that config show loaded --json outputs valid JSON."""
# Create a temporary config file
config_file = tmp_path / "test_config.toml"
config_file.write_text(
"""
[engine]
default = "ollama"
[intelligence]
default_model = "test-model"
temperature = 0.7
"""
)
result = CliRunner().invoke(
cli, ["config", "show", "loaded", "--path", str(config_file), "--json"]
)
assert result.exit_code == 0
# Try to parse the output as JSON (may have prefix lines)
# Find the JSON part by looking for the opening brace
json_start = result.output.find("{")
assert json_start >= 0, f"Could not find JSON in output: {result.output}"
try:
json_data = json.loads(result.output[json_start:])
assert "engine" in json_data
assert json_data["engine"]["default"] == "ollama"
except json.JSONDecodeError:
pytest.fail(f"Output is not valid JSON: {result.output}")
def test_config_show_toml_displays_raw_content(self, tmp_path: Path) -> None:
"""Test that config show toml displays the raw TOML content."""
# Create a temporary config file
config_file = tmp_path / "test_config.toml"
config_file.write_text('[engine]\ndefault = "ollama"\n')
result = CliRunner().invoke(
cli, ["config", "show", "toml", "--path", str(config_file)]
)
assert result.exit_code == 0
assert "[engine]" in result.output
assert "ollama" in result.output
def test_config_show_json_displays_parsed_content(self, tmp_path: Path) -> None:
"""Test that config show json displays parsed TOML as JSON."""
# Create a temporary config file
config_file = tmp_path / "test_config.toml"
config_file.write_text('[engine]\ndefault = "ollama"\n')
result = CliRunner().invoke(
cli, ["config", "show", "json", "--path", str(config_file)]
)
assert result.exit_code == 0
# The output should be valid JSON (may have prefix lines)
json_start = result.output.find("{")
assert json_start >= 0, f"Could not find JSON in output: {result.output}"
try:
json_data = json.loads(result.output[json_start:])
assert json_data["engine"]["default"] == "ollama"
except json.JSONDecodeError:
pytest.fail(f"Output is not valid JSON: {result.output}")
def test_config_show_hardware_displays_info(self) -> None:
"""Test that config show hardware displays hardware information."""
result = CliRunner().invoke(cli, ["config", "show", "hardware"])
assert result.exit_code == 0
# Should display hardware-related content
output = result.output.lower()
assert "hardware" in output or "cpu" in output or "ram" in output
def test_config_show_default_to_loaded(self, tmp_path: Path) -> None:
"""Test that config show (no subcommand) defaults to loaded."""
# Create a temporary config file
config_file = tmp_path / "test_config.toml"
config_file.write_text('[engine]\ndefault = "ollama"\n')
result = CliRunner().invoke(cli, ["config", "show", "--path", str(config_file)])
assert result.exit_code == 0
# Default behavior should show loaded config
assert "ollama" in result.output
def test_config_show_no_config_file(self, tmp_path: Path) -> None:
"""Test that config show handles missing config file gracefully."""
# Create a path for a non-existent config file
config_file = tmp_path / "nonexistent_config.toml"
result = CliRunner().invoke(
cli, ["config", "show", "toml", "--path", str(config_file)]
)
# Should exit 0 and show a message about missing config
assert result.exit_code == 0
assert "not found" in result.output.lower() or "config" in result.output.lower()
def test_config_show_path_option(self, tmp_path: Path) -> None:
"""Test that the --path option works for all subcommands."""
# Create a custom config file
custom_config = tmp_path / "custom.toml"
custom_config.write_text('[engine]\ndefault = "custom-engine"\n')
# Test various subcommands with custom path
result = CliRunner().invoke(
cli, ["config", "show", "toml", "--path", str(custom_config)]
)
assert result.exit_code == 0
assert "custom-engine" in result.output
result = CliRunner().invoke(
cli, ["config", "show", "json", "--path", str(custom_config)]
)
assert result.exit_code == 0
assert "custom-engine" in result.output
def test_config_show_invalid_subcommand(self) -> None:
"""Test that an invalid subcommand shows an error."""
result = CliRunner().invoke(cli, ["config", "show", "invalid_subcommand_xyz"])
# Should show error about unknown command
assert result.exit_code != 0
assert "no such" in result.output.lower() or "unknown" in result.output.lower()
def test_config_subcommands_not_available_standalone(self) -> None:
"""Test that config subcommands are not available directly under config."""
# These should not be available as direct children of config
result = CliRunner().invoke(cli, ["config", "loaded"])
assert result.exit_code != 0
assert "no such" in result.output.lower() or "unknown" in result.output.lower()
+154
View File
@@ -0,0 +1,154 @@
"""Tests for the ``jarvis registry`` CLI commands."""
from __future__ import annotations
from click.testing import CliRunner
from openjarvis.cli import cli
from openjarvis.core.registry import (
ToolRegistry,
)
class TestRegistryCmd:
"""Test cases for the jarvis registry CLI group."""
def test_registry_group_help(self) -> None:
"""Test that the registry group help displays correctly."""
result = CliRunner().invoke(cli, ["registry", "--help"])
assert result.exit_code == 0
assert "registry" in result.output.lower()
def test_registry_list_help(self) -> None:
"""Test that the registry list help displays correctly."""
result = CliRunner().invoke(cli, ["registry", "list", "--help"])
assert result.exit_code == 0
def test_registry_show_help(self) -> None:
"""Test that the registry show help displays correctly."""
result = CliRunner().invoke(cli, ["registry", "show", "--help"])
assert result.exit_code == 0
def test_registry_list_shows_all_registries(self) -> None:
"""Test that registry list displays all available registries."""
result = CliRunner().invoke(cli, ["registry", "list"])
assert result.exit_code == 0
# Should show registry-related content
output = result.output.lower()
assert "registry" in output
def test_registry_show_unknown_registry(self) -> None:
"""Test that showing an unknown registry shows an error."""
result = CliRunner().invoke(cli, ["registry", "show", "unknown_registry_xyz"])
assert result.exit_code == 0 # CLI still exits 0, just shows error message
assert (
"unknown" in result.output.lower() or "not found" in result.output.lower()
)
def test_registry_show_tool_registry(self) -> None:
"""Test that showing the tool registry displays entries."""
# Trigger tool registration
import openjarvis.tools # noqa: F401
result = CliRunner().invoke(cli, ["registry", "show", "tool"])
assert result.exit_code == 0
# Should show tool-related content
assert "tool" in result.output.lower() or "key" in result.output.lower()
def test_registry_show_tool_registry_verbose(self) -> None:
"""Test that showing the tool registry with verbose flag shows details."""
# Trigger tool registration
import openjarvis.tools # noqa: F401
result = CliRunner().invoke(cli, ["registry", "show", "tool", "-v"])
assert result.exit_code == 0
def test_registry_show_empty_registry(self) -> None:
"""Test registry show behavior with an empty registry."""
result = CliRunner().invoke(cli, ["registry", "show", "agent"])
assert result.exit_code == 0
# Should handle empty registries gracefully
assert "agent" in result.output.lower() or "empty" in result.output.lower()
def test_registry_show_accepts_aliases(self) -> None:
"""Test that registry show accepts various aliases."""
# Trigger tool registration
import openjarvis.tools # noqa: F401
# Test with 'tools' alias
result = CliRunner().invoke(cli, ["registry", "show", "tools"])
assert result.exit_code == 0
def test_registry_keys_match_actual_registries(self) -> None:
"""Test that the list command shows all expected registry names."""
result = CliRunner().invoke(cli, ["registry", "list"])
assert result.exit_code == 0
output = result.output
# Check that key registries are mentioned
assert "ToolRegistry" in output or "tool" in output.lower()
assert "EngineRegistry" in output or "engine" in output.lower()
assert "MemoryRegistry" in output or "memory" in output.lower()
assert "ChannelRegistry" in output or "channel" in output.lower()
def test_registry_show_nonexistent_key(self) -> None:
"""Test that showing a nonexistent key in a registry is handled."""
result = CliRunner().invoke(cli, ["registry", "show", "nonexistent"])
assert result.exit_code == 0
assert (
"unknown" in result.output.lower() or "not found" in result.output.lower()
)
def test_registry_list_handles_import_error(self) -> None:
"""Test that registry list handles import errors gracefully."""
# This tests the exception handling path in list_registries
result = CliRunner().invoke(cli, ["registry", "list"])
assert result.exit_code == 0
# Should still complete even if some registries have issues
def test_registry_show_handles_exception(self) -> None:
"""Test that registry show handles exceptions gracefully."""
# Patch the registry to raise an exception during keys() call
from unittest.mock import patch
with patch(
"openjarvis.core.registry.ToolRegistry.keys",
side_effect=Exception("Test error"),
):
result = CliRunner().invoke(cli, ["registry", "show", "tool"])
assert result.exit_code == 0
assert "error" in result.output.lower()
def test_registry_list_handles_registry_error(self) -> None:
"""Test that registry list handles errors for specific registries."""
from unittest.mock import patch
# Make one of the registry classes raise an error during keys()
with patch.object(ToolRegistry, "keys", side_effect=Exception("Import error")):
result = CliRunner().invoke(cli, ["registry", "list"])
assert result.exit_code == 0
# Should still complete and show other registries
def test_registry_show_handles_error_during_import(self) -> None:
"""Test that registry show handles exceptions during import."""
from unittest.mock import patch
# Patch the entire import to raise an exception
with patch.dict("sys.modules", {"openjarvis.core.registry": None}):
# This tests the outer exception handler
result = CliRunner().invoke(cli, ["registry", "show", "tool"])
assert result.exit_code == 0
def test_registry_show_handles_error_during_iteration(self) -> None:
"""Test that registry show handles exceptions during entry iteration."""
from unittest.mock import patch
# Patch keys to return a fake entry so iteration happens,
# then patch get to raise
with patch.object(ToolRegistry, "keys", return_value=["fake_tool"]):
with patch.object(
ToolRegistry, "get", side_effect=Exception("Iteration error")
):
result = CliRunner().invoke(cli, ["registry", "show", "tool"])
assert result.exit_code == 0
assert "error" in result.output.lower()
+238
View File
@@ -0,0 +1,238 @@
"""Tests for the ``jarvis tool`` CLI commands."""
from __future__ import annotations
from click.testing import CliRunner
from openjarvis.cli import cli
from openjarvis.core.registry import ToolRegistry
class TestToolCmd:
"""Test cases for the jarvis tool CLI group."""
def test_tool_group_help(self) -> None:
"""Test that the tool group help displays correctly."""
result = CliRunner().invoke(cli, ["tool", "--help"])
assert result.exit_code == 0
assert "tool" in result.output.lower() or "list" in result.output
def test_tool_list_help(self) -> None:
"""Test that the tool list help displays correctly."""
result = CliRunner().invoke(cli, ["tool", "list", "--help"])
assert result.exit_code == 0
assert "list" in result.output.lower() or "registered" in result.output.lower()
def test_tool_inspect_help(self) -> None:
"""Test that the tool inspect help displays correctly."""
result = CliRunner().invoke(cli, ["tool", "inspect", "--help"])
assert result.exit_code == 0
assert "tool" in result.output.lower() or "inspect" in result.output.lower()
def test_tool_list_shows_registered_tools(self) -> None:
"""Test that tool list displays registered tools."""
result = CliRunner().invoke(cli, ["tool", "list"])
assert result.exit_code == 0
# Should show at least some tools if they're registered
output = result.output.lower()
# The output should contain a table header or tool-related content
assert "tool" in output or "name" in output or "description" in output
def test_tool_inspect_unknown_tool(self) -> None:
"""Test that inspecting an unknown tool shows an error."""
result = CliRunner().invoke(cli, ["tool", "inspect", "nonexistent_tool_xyz"])
assert result.exit_code == 0 # CLI still exits 0, just shows error message
assert "not found" in result.output.lower() or "error" in result.output.lower()
def test_tool_inspect_known_tool(self) -> None:
"""Test that inspecting a known tool shows details."""
# First, trigger tool registration
import openjarvis.tools # noqa: F401
# Get a known tool name
registered_tools = ToolRegistry.keys()
if registered_tools:
tool_name = registered_tools[0]
result = CliRunner().invoke(cli, ["tool", "inspect", tool_name])
assert result.exit_code == 0
assert tool_name in result.output
def test_tool_list_empty_registry(self) -> None:
"""Test tool list behavior with empty registry."""
# This test verifies the command runs even if no tools are registered
result = CliRunner().invoke(cli, ["tool", "list"])
assert result.exit_code == 0
def test_tool_inspect_requires_tool_name(self) -> None:
"""Test that inspect command requires a tool name argument."""
# Running inspect without a tool name should show help or error
result = CliRunner().invoke(cli, ["tool", "inspect"])
# Either shows help (exit 0) or error (exit non-zero)
assert result.exit_code in (0, 2) # 0 for help, 2 for missing argument
def test_tool_list_with_registered_tools(self) -> None:
"""Test that tool list shows details for registered tools."""
# Trigger tool registration
import openjarvis.tools # noqa: F401
result = CliRunner().invoke(cli, ["tool", "list"])
assert result.exit_code == 0
# Should either show tools or indicate no tools are registered
output = result.output
assert (
"Registered Tools" in output
or "No tools registered" in output
or "Total:" in output
)
def test_tool_list_handles_instantiation_error(self) -> None:
"""Test that tool list handles tool instantiation errors gracefully."""
from unittest.mock import patch
# Mock a tool class that raises an exception during instantiation
with patch.object(ToolRegistry, "keys", return_value=["mock_tool"]):
with patch.object(ToolRegistry, "get", return_value=object):
result = CliRunner().invoke(cli, ["tool", "list"])
assert result.exit_code == 0
# Should still complete even if tools have instantiation issues
assert "mock_tool" in result.output or "Total:" in result.output
def test_tool_inspect_with_spec_details(self) -> None:
"""Test that inspect shows full spec details for tools with specs."""
import openjarvis.tools # noqa: F401
registered_tools = ToolRegistry.keys()
if registered_tools:
tool_name = registered_tools[0]
result = CliRunner().invoke(cli, ["tool", "inspect", tool_name])
assert result.exit_code == 0
# Should show tool details including name, description, category
output = result.output
assert tool_name in output
def test_tool_inspect_handles_instantiation_error(self) -> None:
"""Test that inspect handles tool instantiation errors gracefully."""
from unittest.mock import patch
# Mock a tool that exists but fails to instantiate
with patch.object(ToolRegistry, "contains", return_value=True):
with patch.object(ToolRegistry, "get", return_value=object):
result = CliRunner().invoke(cli, ["tool", "inspect", "mock_tool"])
assert result.exit_code == 0
# Should show error note but still complete
assert "mock_tool" in result.output
def test_tool_list_catches_registry_exception(self) -> None:
"""Test that tool list handles exceptions during registry access."""
from unittest.mock import patch
# Mock ToolRegistry.keys to raise an exception
with patch.object(
ToolRegistry, "keys", side_effect=Exception("Registry error")
):
result = CliRunner().invoke(cli, ["tool", "list"])
assert result.exit_code == 0
# Should catch exception and display error message
assert "error" in result.output.lower()
def test_tool_inspect_catches_registry_exception(self) -> None:
"""Test that tool inspect handles exceptions during registry access."""
from unittest.mock import patch
# Mock ToolRegistry.contains to raise an exception
with patch.object(
ToolRegistry, "contains", side_effect=Exception("Registry error")
):
result = CliRunner().invoke(cli, ["tool", "inspect", "mock_tool"])
assert result.exit_code == 0
# Should catch exception and display error message
assert "error" in result.output.lower()
def test_tool_list_shows_tool_spec(self) -> None:
"""Test that tool list displays spec details when available."""
from unittest.mock import MagicMock, patch
# Create a mock tool class with a spec
mock_spec = MagicMock()
mock_spec.description = "Test tool description"
mock_spec.category = "test-category"
mock_tool_cls = MagicMock()
mock_tool_instance = MagicMock()
mock_tool_instance.spec = mock_spec
mock_tool_cls.return_value = mock_tool_instance
with patch.object(ToolRegistry, "keys", return_value=["mock_tool"]):
with patch.object(ToolRegistry, "get", return_value=mock_tool_cls):
result = CliRunner().invoke(cli, ["tool", "list"])
assert result.exit_code == 0
# Should show spec details
output = result.output
assert "mock_tool" in output
assert "Test tool description" in output
def test_tool_inspect_shows_full_spec_details(self) -> None:
"""Test that inspect displays full spec details including parameters."""
from unittest.mock import MagicMock, patch
# Create a mock tool with full spec including parameters
mock_spec = MagicMock()
mock_spec.name = "MockTool"
mock_spec.description = "A mock tool for testing"
mock_spec.category = "testing"
mock_spec.parameters = {
"properties": {
"input": {"type": "string", "description": "Input parameter"},
"count": {"type": "integer", "description": "Count value"},
}
}
mock_tool_cls = MagicMock()
mock_tool_instance = MagicMock()
mock_tool_instance.spec = mock_spec
mock_tool_cls.return_value = mock_tool_instance
with patch.object(ToolRegistry, "contains", return_value=True):
with patch.object(ToolRegistry, "get", return_value=mock_tool_cls):
result = CliRunner().invoke(cli, ["tool", "inspect", "mock_tool"])
assert result.exit_code == 0
output = result.output
assert "mock_tool" in output
assert "MockTool" in output
assert "A mock tool for testing" in output
assert "testing" in output
assert "Parameters" in output
assert "input" in output
assert "count" in output
def test_tool_inspect_handles_tool_without_spec(self) -> None:
"""Test that inspect handles tools without spec attribute."""
from unittest.mock import MagicMock, patch
# Mock a tool class without spec attribute
mock_tool_cls = MagicMock()
del mock_tool_cls.spec # Remove spec attribute
with patch.object(ToolRegistry, "contains", return_value=True):
with patch.object(ToolRegistry, "get", return_value=mock_tool_cls):
result = CliRunner().invoke(cli, ["tool", "inspect", "mock_tool"])
assert result.exit_code == 0
# Should handle gracefully
assert "mock_tool" in result.output
def test_tool_list_handles_tool_without_spec(self) -> None:
"""Test that tool list handles tools without spec attribute."""
from unittest.mock import patch
# Create a simple class without spec attribute
class ToolWithoutSpec:
pass
mock_tool_cls = ToolWithoutSpec
with patch.object(ToolRegistry, "keys", return_value=["mock_tool"]):
with patch.object(ToolRegistry, "get", return_value=mock_tool_cls):
result = CliRunner().invoke(cli, ["tool", "list"])
assert result.exit_code == 0
# Should handle gracefully and show the tool name
assert "mock_tool" in result.output or "Total" in result.output