Phase 4: Shell completion scripts and comprehensive test suite

This commit is contained in:
truetype committed 2026-03-20 17:54:46 +01:00
1 parent aba2fc11f4
commit 5e70e22780
5 files changed
+641 -7

No files matched your search

+32 -7
View File
@@ -11,6 +11,8 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM.
- **Estensibile**: Aggiungi strumenti personalizzati, modifica i prompt e configura i sub-agenti secondo le tue esigenze
- **Strumenti unificati**: Gestisci qualsiasi provider con la stessa interfaccia
- **Strumenti personalizzati**: Include strumenti per gestire Proxmox e Ollama
- **Completamento shell**: Supporta bash e zsh per autocompletamento dei comandi
- **Test completi**: Suite di test per garantire la qualità del codice
## Installazione
@@ -20,6 +22,21 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM.
pip install -r requirements.txt
```
### Configurazione del Completamento Shell
#### Bash
```bash
# Aggiungi al tuo ~/.bashrc o ~/.bash_profile
source /path/to/maxclaw_agent/completions/maxclaw.bash
```
#### Zsh
```bash
# Aggiungi al tuo ~/.zshrc
fpath=(/path/to/maxclaw_agent/completions $fpath)
autoload -Uz compinit && compinit
```
## Configurazione
Puoi configurare MaxClaw Agent in diversi modi:
@@ -202,6 +219,21 @@ Gli strumenti Ollama specifici sono ancora disponibili per retrocompatibilità:
- `ollama_status`: Controlla se Ollama è in esecuzione
- `ollama_running_models`: Mostra i modelli attualmente in esecuzione
## Test
Esegui la suite di test:
```bash
# Esegui tutti i test
pytest tests/
# Esegui test specifici
pytest tests/test_config_manager.py
pytest tests/test_provider_tools.py
# Esegui con output verboso
pytest tests/ -v
```
## Personalizzazione
Per personalizzare l'agente, modifica `main.py`:
@@ -210,13 +242,6 @@ Per personalizzare l'agente, modifica `main.py`:
- Aggiungi strumenti personalizzati alla chiamata `create_deep_agent`
- Configura i sub-agenti o altre funzionalità di DeepAgents
## Test
Esegui lo script di test:
```bash
python test_agent.py
```
## Sicurezza
MaxClaw Agent implementa misure di sicurezza avanzate:
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# Bash completion for MaxClaw Agent CLI
_maxclaw_completions()
{
local cur prev commands
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Main commands
commands="list search add remove set-active active test export import help"
# Handle main command completion
if [[ ${COMP_CWORD} -eq 1 ]]; then
COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
return 0
fi
# Handle subcommand-specific completions
case "${prev}" in
search)
# Search term - no completion
return 0
;;
add)
# Add command options
local add_options="--non-interactive --name --provider --model --api-key --base-url --set-active"
COMPREPLY=( $(compgen -W "${add_options}" -- ${cur}) )
return 0
;;
remove|set-active|test)
# Configuration names - try to get from config list
if command -v python3 &> /dev/null; then
local configs=$(python3 -c "
import sys
sys.path.insert(0, '.')
try:
from config_manager import config_manager
configs = config_manager.list_configuration_names()
print(' '.join(configs))
except:
pass
" 2>/dev/null)
COMPREPLY=( $(compgen -W "${configs}" -- ${cur}) )
fi
return 0
;;
export)
# Export options
local export_options="--format --output"
COMPREPLY=( $(compgen -W "${export_options}" -- ${cur}) )
return 0
;;
import)
# File completion
COMPREPLY=( $(compgen -f -- ${cur}) )
return 0
;;
--provider)
# Provider options
local providers="openai ollama openrouter"
COMPREPLY=( $(compgen -W "${providers}" -- ${cur}) )
return 0
;;
--format)
# Format options
local formats="json yaml"
COMPREPLY=( $(compgen -W "${formats}" -- ${cur}) )
return 0
;;
esac
# Handle option arguments
case "${cur}" in
-*)
# Option completion
local options="--non-interactive --name --provider --model --api-key --base-url --set-active --format --output"
COMPREPLY=( $(compgen -W "${options}" -- ${cur}) )
return 0
;;
esac
return 0
}
complete -F _maxclaw_completions python3 config_cli.py
complete -F _maxclaw_completions python config_cli.py
+79
View File
@@ -0,0 +1,79 @@
#compdef config_cli.py
# Zsh completion for MaxClaw Agent CLI
_maxclaw() {
local -a commands
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments -C \
'1: :->command' \
'*::arg:->args'
case $state in
command)
commands=(
'list:List all configurations'
'search:Search configurations'
'add:Add a new configuration'
'remove:Remove a configuration'
'set-active:Set active configuration'
'active:Show active configuration'
'test:Test connection to a configuration'
'export:Export configurations to JSON/YAML'
'import:Import configurations from JSON/YAML'
'help:Show help'
)
_describe -t commands 'MaxClaw commands' commands
;;
args)
case ${line[1]} in
search)
_message 'search term'
;;
add)
_arguments \
'--non-interactive[Non-interactive mode]' \
'--name[Configuration name]:name' \
'--provider[Provider]:provider:(openai ollama openrouter)' \
'--model[Model name]:model' \
'--api-key[API key]:api_key' \
'--base-url[Base URL]:base_url' \
'--set-active[Set as active configuration]'
;;
remove|set-active|test)
# Try to get configuration names
local -a configs
if (( $+commands[python3] )); then
configs=(${(f)"$(python3 -c "
import sys
sys.path.insert(0, '.')
try:
from config_manager import config_manager
configs = config_manager.list_configuration_names()
for c in configs:
print(c)
except:
pass
" 2>/dev/null)"})
fi
if (( ${#configs} )); then
_describe -t configurations 'configurations' configs
else
_message 'configuration name'
fi
;;
export)
_arguments \
'--format[Export format]:format:(json yaml)' \
'--output[Output filename]:filename'
;;
import)
_files -g '*.json *.yaml *.yml'
;;
esac
;;
esac
}
_maxclaw "$@"
+186
View File
@@ -0,0 +1,186 @@
"""
Test suite for MaxClaw Agent configuration manager
"""
import os
import json
import tempfile
import pytest
from pathlib import Path
import sys
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config_manager import ConfigManager
class TestConfigManager:
"""Test cases for ConfigManager"""
def setup_method(self):
"""Set up test environment"""
self.temp_dir = tempfile.mkdtemp()
self.config_manager = ConfigManager(self.temp_dir)
def teardown_method(self):
"""Clean up test environment"""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_initialization(self):
"""Test ConfigManager initialization"""
assert self.config_manager.config_dir.exists()
assert self.config_manager.config_file.exists()
assert self.config_manager.key_file.exists()
assert self.config_manager.config == {"configurations": {}, "active_configuration": None, "version": "1.0"}
def test_add_configuration(self):
"""Test adding a configuration"""
config = {
"provider": "ollama",
"model_name": "gemma2:9b",
"base_url": "http://localhost:11434"
}
self.config_manager.add_configuration("test-config", config)
# Verify configuration was added
assert self.config_manager.configuration_exists("test-config")
retrieved = self.config_manager.get_configuration("test-config")
assert retrieved["provider"] == "ollama"
assert retrieved["model_name"] == "gemma2:9b"
assert retrieved["base_url"] == "http://localhost:11434"
def test_remove_configuration(self):
"""Test removing a configuration"""
config = {
"provider": "openai",
"model_name": "gpt-4o",
"api_key": "sk-test123"
}
self.config_manager.add_configuration("test-openai", config)
assert self.config_manager.configuration_exists("test-openai")
# Remove configuration
result = self.config_manager.remove_configuration("test-openai")
assert result is True
assert not self.config_manager.configuration_exists("test-openai")
def test_set_active_configuration(self):
"""Test setting active configuration"""
config = {
"provider": "ollama",
"model_name": "gemma2:9b",
"base_url": "http://localhost:11434"
}
self.config_manager.add_configuration("test-config", config)
# Set as active
result = self.config_manager.set_active_configuration("test-config")
assert result is True
assert self.config_manager.get_active_configuration() == "test-config"
def test_validation(self):
"""Test configuration validation"""
# Valid configuration
valid_config = {
"provider": "ollama",
"model_name": "gemma2:9b",
"base_url": "http://localhost:11434"
}
is_valid, errors = self.config_manager.validate_configuration(valid_config)
assert is_valid is True
assert len(errors) == 0
# Invalid configuration - missing provider
invalid_config = {
"model_name": "gemma2:9b",
"base_url": "http://localhost:11434"
}
is_valid, errors = self.config_manager.validate_configuration(invalid_config)
assert is_valid is False
assert "Provider is required" in errors
# Invalid configuration - OpenAI without API key
invalid_openai = {
"provider": "openai",
"model_name": "gpt-4o"
}
is_valid, errors = self.config_manager.validate_configuration(invalid_openai)
assert is_valid is False
assert "API key is required for OpenAI provider" in errors
def test_encryption(self):
"""Test that API keys are encrypted"""
config = {
"provider": "openai",
"model_name": "gpt-4o",
"api_key": "sk-test123456789"
}
self.config_manager.add_configuration("test-openai", config)
# Read the raw config file
with open(self.config_manager.config_file, 'r') as f:
raw_config = json.load(f)
# Check that API key is encrypted in the file
stored_config = raw_config["configurations"]["test-openai"]
assert "api_key" in stored_config
assert stored_config["api_key"] != "sk-test123456789" # Should be encrypted
assert len(stored_config["api_key"]) > 20 # Encrypted data is longer
# But when retrieved, it should be decrypted
retrieved = self.config_manager.get_configuration("test-openai")
assert retrieved["api_key"] == "sk-test123456789"
def test_list_configurations(self):
"""Test listing configurations"""
# Add multiple configurations
configs = [
("config1", {"provider": "ollama", "model_name": "gemma2:9b", "base_url": "http://localhost:11434"}),
("config2", {"provider": "openai", "model_name": "gpt-4o", "api_key": "sk-test"}),
("config3", {"provider": "openrouter", "model_name": "claude-3.5-sonnet", "api_key": "sk-or-test", "base_url": "https://openrouter.ai/api/v1"})
]
for name, config in configs:
self.config_manager.add_configuration(name, config)
# List configurations
config_names = self.config_manager.list_configuration_names()
assert len(config_names) == 3
assert "config1" in config_names
assert "config2" in config_names
assert "config3" in config_names
def test_backup_creation(self):
"""Test that backups are created"""
config = {
"provider": "ollama",
"model_name": "gemma2:9b",
"base_url": "http://localhost:11434"
}
# Add first configuration
self.config_manager.add_configuration("config1", config)
# Add second configuration (should create backup)
config2 = {
"provider": "openai",
"model_name": "gpt-4o",
"api_key": "sk-test"
}
self.config_manager.add_configuration("config2", config2)
# Check that backup file exists
backup_file = self.config_manager.config_file.with_suffix('.json.bak')
assert backup_file.exists()
if __name__ == "__main__":
pytest.main([__file__, "-v"])
+256
View File
@@ -0,0 +1,256 @@
"""
Test suite for MaxClaw Agent provider tools
"""
import os
import sys
import pytest
from unittest.mock import Mock, patch, MagicMock
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from tools.provider_tools import (
OllamaProviderTools,
OpenAIProviderTools,
OpenRouterProviderTools,
get_provider_tools
)
class TestOllamaProviderTools:
"""Test cases for OllamaProviderTools"""
def setup_method(self):
"""Set up test environment"""
self.tools = OllamaProviderTools(base_url="http://localhost:11434")
def test_initialization(self):
"""Test OllamaProviderTools initialization"""
assert self.tools.provider_name == "ollama"
assert self.tools.base_url == "http://localhost:11434"
assert self.tools.api_url == "http://localhost:11434/api"
@patch('requests.get')
def test_list_models_success(self, mock_get):
"""Test successful model listing"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"models": [
{"name": "gemma2:9b", "size": 5000000000},
{"name": "llama2:7b", "size": 4000000000}
]
}
mock_get.return_value = mock_response
result = self.tools.list_models()
assert "Available Ollama Models" in result
assert "gemma2:9b" in result
assert "llama2:7b" in result
@patch('requests.get')
def test_list_models_failure(self, mock_get):
"""Test failed model listing"""
mock_get.side_effect = Exception("Connection failed")
result = self.tools.list_models()
assert "Error listing models" in result
@patch('requests.post')
def test_get_model_info_success(self, mock_post):
"""Test successful model info retrieval"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"license": "Apache 2.0",
"parameters": "9B",
"details": {
"parameter_size": "9B",
"quantization_level": "Q4_0"
}
}
mock_post.return_value = mock_response
result = self.tools.get_model_info("gemma2:9b")
assert "Model Information: gemma2:9b" in result
assert "License: Apache 2.0" in result
assert "Parameter Size: 9B" in result
@patch('requests.get')
def test_test_connection_success(self, mock_get):
"""Test successful connection test"""
mock_response = Mock()
mock_response.status_code = 200
mock_get.return_value = mock_response
result = self.tools.test_connection()
assert "✅ Ollama instance at http://localhost:11434 is running" in result
@patch('requests.get')
def test_test_connection_failure(self, mock_get):
"""Test failed connection test"""
mock_get.side_effect = Exception("Connection failed")
result = self.tools.test_connection()
assert "❌ Cannot connect to Ollama instance" in result
def test_get_tools(self):
"""Test tool generation"""
tools = self.tools.get_tools()
assert len(tools) == 6
tool_names = [tool.name for tool in tools]
assert "ollama_list_models" in tool_names
assert "ollama_model_info" in tool_names
assert "ollama_test_connection" in tool_names
assert "ollama_get_default_model" in tool_names
assert "ollama_set_default_model" in tool_names
assert "ollama_info" in tool_names
class TestOpenAIProviderTools:
"""Test cases for OpenAIProviderTools"""
def setup_method(self):
"""Set up test environment"""
self.tools = OpenAIProviderTools(api_key="sk-test123")
def test_initialization(self):
"""Test OpenAIProviderTools initialization"""
assert self.tools.provider_name == "openai"
assert self.tools.api_key == "sk-test123"
assert self.tools.api_url == "https://api.openai.com/v1"
@patch('requests.get')
def test_list_models_success(self, mock_get):
"""Test successful model listing"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": [
{"id": "gpt-4o"},
{"id": "gpt-4o-mini"},
{"id": "gpt-3.5-turbo"}
]
}
mock_get.return_value = mock_response
result = self.tools.list_models()
assert "Available OpenAI Models" in result
assert "gpt-4o" in result
assert "gpt-4o-mini" in result
@patch('requests.get')
def test_test_connection_success(self, mock_get):
"""Test successful connection test"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": []}
mock_get.return_value = mock_response
result = self.tools.test_connection()
assert "✅ OpenAI API connection successful" in result
def test_get_tools(self):
"""Test tool generation"""
tools = self.tools.get_tools()
assert len(tools) == 6
tool_names = [tool.name for tool in tools]
assert "openai_list_models" in tool_names
assert "openai_model_info" in tool_names
assert "openai_test_connection" in tool_names
class TestOpenRouterProviderTools:
"""Test cases for OpenRouterProviderTools"""
def setup_method(self):
"""Set up test environment"""
self.tools = OpenRouterProviderTools(
api_key="sk-or-test123",
base_url="https://openrouter.ai/api/v1"
)
def test_initialization(self):
"""Test OpenRouterProviderTools initialization"""
assert self.tools.provider_name == "openrouter"
assert self.tools.api_key == "sk-or-test123"
assert self.tools.base_url == "https://openrouter.ai/api/v1"
@patch('requests.get')
def test_list_models_success(self, mock_get):
"""Test successful model listing"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": [
{"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5 Sonnet"},
{"id": "openai/gpt-4o", "name": "GPT-4o"}
]
}
mock_get.return_value = mock_response
result = self.tools.list_models()
assert "Available OpenRouter Models" in result
assert "anthropic/claude-3.5-sonnet" in result
assert "openai/gpt-4o" in result
def test_get_tools(self):
"""Test tool generation"""
tools = self.tools.get_tools()
assert len(tools) == 6
tool_names = [tool.name for tool in tools]
assert "openrouter_list_models" in tool_names
assert "openrouter_model_info" in tool_names
assert "openrouter_test_connection" in tool_names
class TestGetProviderTools:
"""Test cases for get_provider_tools function"""
@patch.dict(os.environ, {"OLLAMA_BASE_URL": "http://localhost:11434"})
def test_get_ollama_tools(self):
"""Test getting Ollama tools"""
tools = get_provider_tools("ollama")
assert len(tools) == 6
tool_names = [tool.name for tool in tools]
assert "ollama_list_models" in tool_names
@patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test123"})
def test_get_openai_tools(self):
"""Test getting OpenAI tools"""
tools = get_provider_tools("openai")
assert len(tools) == 6
tool_names = [tool.name for tool in tools]
assert "openai_list_models" in tool_names
@patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-or-test123"})
def test_get_openrouter_tools(self):
"""Test getting OpenRouter tools"""
tools = get_provider_tools("openrouter")
assert len(tools) == 6
tool_names = [tool.name for tool in tools]
assert "openrouter_list_models" in tool_names
def test_get_unknown_provider_tools(self):
"""Test getting tools for unknown provider"""
tools = get_provider_tools("unknown")
assert len(tools) == 0
if __name__ == "__main__":
pytest.main([__file__, "-v"])