Update SSH access patterns documentation and expand workflow integration test suite
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
Documentation Validation / validate-policies-strict (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Coverage Phase 1 (70% Target) / test-coverage-70 (push) Has been cancelled
Coverage Phase 2 (85% Target) / test-coverage-85 (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
Documentation Validation / validate-policies-strict (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Coverage Phase 1 (70% Target) / test-coverage-70 (push) Has been cancelled
Coverage Phase 2 (85% Target) / test-coverage-85 (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
- ssh-access-patterns.md: Clarify ns3/aitbc container setup with correct paths and service names - Add container hostname verification command - Update paths: /etc/aitbc/blockchain.env, /opt/aitbc/apps/blockchain-node/ - Fix service name: aitbc-blockchain-node (not aitbc-blockchain-node-3) - Add service restart and log viewing examples - test_workflow.sh: Rewrite as comprehensive integration test suite - Add
This commit is contained in:
311
tests/cli/test_config_profiles.py
Normal file
311
tests/cli/test_config_profiles.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""Integration tests for config profiles CLI commands
|
||||
|
||||
These tests require no running services but validate file system side effects
|
||||
and actual profile CRUD operations.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch
|
||||
from aitbc_cli.commands.config import config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
config = Mock()
|
||||
config.coordinator_url = "http://127.0.0.1:18000"
|
||||
config.api_key = None
|
||||
config.timeout = 30
|
||||
config.config_file = "/home/oib/.aitbc/config.yaml"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def profiles_dir(tmp_path):
|
||||
"""Create and return profiles directory"""
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
profiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
return profiles_dir
|
||||
|
||||
|
||||
class TestConfigProfilesIntegration:
|
||||
"""Integration tests for config profiles with file system validation"""
|
||||
|
||||
def test_profiles_save_creates_file(self, runner, mock_config, profiles_dir):
|
||||
"""Test saving a profile creates the correct file"""
|
||||
profile_name = "test_profile"
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'save', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert f"Profile '{profile_name}' saved" in result.output
|
||||
|
||||
# Verify file was created
|
||||
profile_file = profiles_dir / f"{profile_name}.yaml"
|
||||
assert profile_file.exists()
|
||||
|
||||
# Verify file content
|
||||
with open(profile_file) as f:
|
||||
profile_data = yaml.safe_load(f)
|
||||
assert profile_data['coordinator_url'] == 'http://127.0.0.1:18000'
|
||||
assert profile_data['timeout'] == 30
|
||||
assert 'api_key' not in profile_data # API key should not be saved
|
||||
|
||||
def test_profiles_save_overwrites_existing(self, runner, mock_config, profiles_dir):
|
||||
"""Test saving a profile overwrites existing profile"""
|
||||
profile_name = "overwrite_test"
|
||||
|
||||
# Create existing profile
|
||||
profile_file = profiles_dir / f"{profile_name}.yaml"
|
||||
profile_file.write_text(yaml.dump({
|
||||
"coordinator_url": "http://old:8000",
|
||||
"timeout": 10
|
||||
}))
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'save', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify file was overwritten
|
||||
with open(profile_file) as f:
|
||||
profile_data = yaml.safe_load(f)
|
||||
assert profile_data['coordinator_url'] == 'http://127.0.0.1:18000'
|
||||
assert profile_data['timeout'] == 30
|
||||
|
||||
def test_profiles_list_empty(self, runner, mock_config, profiles_dir):
|
||||
"""Test listing profiles when none exist"""
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
import json
|
||||
data = json.loads(result.output)
|
||||
assert data['profiles'] == []
|
||||
|
||||
def test_profiles_list_multiple(self, runner, mock_config, profiles_dir):
|
||||
"""Test listing multiple profiles"""
|
||||
# Create test profiles
|
||||
profile1 = profiles_dir / "profile1.yaml"
|
||||
profile1.write_text(yaml.dump({
|
||||
"coordinator_url": "http://test1:8000",
|
||||
"timeout": 30
|
||||
}))
|
||||
|
||||
profile2 = profiles_dir / "profile2.yaml"
|
||||
profile2.write_text(yaml.dump({
|
||||
"coordinator_url": "http://test2:8000",
|
||||
"timeout": 60
|
||||
}))
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['profiles']) == 2
|
||||
assert data['profiles'][0]['name'] == 'profile1'
|
||||
assert data['profiles'][1]['name'] == 'profile2'
|
||||
|
||||
def test_profiles_load_creates_config(self, runner, mock_config, profiles_dir, tmp_path):
|
||||
"""Test loading a profile creates config file"""
|
||||
profile_name = "load_test"
|
||||
|
||||
# Create profile
|
||||
profile_file = profiles_dir / f"{profile_name}.yaml"
|
||||
profile_file.write_text(yaml.dump({
|
||||
"coordinator_url": "http://loaded:8000",
|
||||
"timeout": 45
|
||||
}))
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'load', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert f"Profile '{profile_name}' loaded" in result.output
|
||||
|
||||
# Verify config file was created
|
||||
config_file = Path.cwd() / ".aitbc.yaml"
|
||||
assert config_file.exists()
|
||||
|
||||
with open(config_file) as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
assert config_data['coordinator_url'] == 'http://loaded:8000'
|
||||
assert config_data['timeout'] == 45
|
||||
|
||||
def test_profiles_load_nonexistent(self, runner, mock_config, profiles_dir):
|
||||
"""Test loading a non-existent profile"""
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'load', 'nonexistent'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "not found" in result.output
|
||||
|
||||
def test_profiles_delete_removes_file(self, runner, mock_config, profiles_dir):
|
||||
"""Test deleting a profile removes the file"""
|
||||
profile_name = "delete_test"
|
||||
|
||||
# Create profile
|
||||
profile_file = profiles_dir / f"{profile_name}.yaml"
|
||||
profile_file.write_text(yaml.dump({
|
||||
"coordinator_url": "http://test:8000",
|
||||
"timeout": 30
|
||||
}))
|
||||
|
||||
assert profile_file.exists()
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'delete', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'table'}, input='y\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert f"Profile '{profile_name}' deleted" in result.output
|
||||
assert not profile_file.exists()
|
||||
|
||||
def test_profiles_delete_cancelled(self, runner, mock_config, profiles_dir):
|
||||
"""Test profile deletion cancelled by user"""
|
||||
profile_name = "keep_test"
|
||||
|
||||
# Create profile
|
||||
profile_file = profiles_dir / f"{profile_name}.yaml"
|
||||
profile_file.write_text(yaml.dump({
|
||||
"coordinator_url": "http://test:8000",
|
||||
"timeout": 30
|
||||
}))
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'delete', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'json'}, input='n\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert profile_file.exists() # Should still exist
|
||||
|
||||
def test_profiles_delete_nonexistent(self, runner, mock_config, profiles_dir):
|
||||
"""Test deleting a non-existent profile"""
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'delete', 'nonexistent'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "not found" in result.output
|
||||
|
||||
def test_profiles_roundtrip(self, runner, mock_config, profiles_dir, tmp_path):
|
||||
"""Test save -> list -> load -> delete roundtrip"""
|
||||
profile_name = "roundtrip_test"
|
||||
|
||||
# Save
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'save', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
assert result.exit_code == 0
|
||||
|
||||
# List
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert profile_name in [p['name'] for p in data['profiles']]
|
||||
|
||||
# Load
|
||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'load', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Delete
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'delete', profile_name
|
||||
], obj={'config': mock_config, 'output_format': 'table'}, input='y\n')
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify deleted
|
||||
profile_file = profiles_dir / f"{profile_name}.yaml"
|
||||
assert not profile_file.exists()
|
||||
|
||||
def test_profiles_with_different_configs(self, runner, mock_config, profiles_dir):
|
||||
"""Test saving profiles with different config values"""
|
||||
# Modify config for different profile
|
||||
mock_config.coordinator_url = "http://different:9000"
|
||||
mock_config.timeout = 90
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = profiles_dir.parent.parent.parent
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'save', 'different_profile'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
profile_file = profiles_dir / "different_profile.yaml"
|
||||
with open(profile_file) as f:
|
||||
profile_data = yaml.safe_load(f)
|
||||
assert profile_data['coordinator_url'] == 'http://different:9000'
|
||||
assert profile_data['timeout'] == 90
|
||||
|
||||
def test_profiles_directory_creation(self, runner, mock_config, tmp_path):
|
||||
"""Test that profiles directory is created if it doesn't exist"""
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
# Don't create it beforehand
|
||||
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles', 'save', 'new_profile'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert profiles_dir.exists()
|
||||
assert (profiles_dir / "new_profile.yaml").exists()
|
||||
359
tests/cli/test_edge_advanced.py
Normal file
359
tests/cli/test_edge_advanced.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""Integration tests for edge advanced CLI commands
|
||||
|
||||
These tests require edge-api running and validate advanced edge operations
|
||||
including island leave/bridge, GPU operations, database operations, serve operations, and metrics.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import httpx
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from aitbc_cli.commands.edge import edge
|
||||
from aitbc import AITBCHTTPClient, NetworkError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
config = Mock()
|
||||
config.coordinator_url = "http://127.0.0.1:18000"
|
||||
config.api_key = "test_api_key"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_http_client():
|
||||
"""Mock HTTP client for edge-api"""
|
||||
client = MagicMock(spec=AITBCHTTPClient)
|
||||
return client
|
||||
|
||||
|
||||
class TestEdgeAdvancedCommands:
|
||||
"""Integration tests for edge advanced commands with edge-api"""
|
||||
|
||||
@pytest.fixture
|
||||
def edge_available(self):
|
||||
"""Skip test if edge-api is not running"""
|
||||
try:
|
||||
response = httpx.get("http://127.0.0.1:8200/health", timeout=2)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
except Exception:
|
||||
pytest.skip("edge-api not running at http://127.0.0.1:8200")
|
||||
|
||||
# Island advanced operations
|
||||
def test_edge_island_leave(self, runner, mock_config, edge_available):
|
||||
"""Test leaving an island"""
|
||||
result = runner.invoke(edge, [
|
||||
'island', 'leave',
|
||||
'--island-id', 'test_island_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'island_id' in data or 'status' in data
|
||||
|
||||
def test_edge_island_bridge(self, runner, mock_config, edge_available):
|
||||
"""Test bridging between islands"""
|
||||
result = runner.invoke(edge, [
|
||||
'island', 'bridge',
|
||||
'--source', 'island_a',
|
||||
'--target', 'island_b'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'bridge_id' in data or 'status' in data
|
||||
|
||||
# GPU operations
|
||||
def test_edge_gpu_list_gpus(self, runner, mock_config, edge_available):
|
||||
"""Test listing GPUs"""
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'list_gpus'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'gpus' in data or isinstance(data, list)
|
||||
|
||||
def test_edge_gpu_get_gpu(self, runner, mock_config, edge_available):
|
||||
"""Test getting specific GPU info"""
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'get_gpu',
|
||||
'--gpu-id', 'gpu_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'gpu_id' in data or 'status' in data
|
||||
|
||||
def test_edge_gpu_remove_gpu(self, runner, mock_config, edge_available):
|
||||
"""Test removing a GPU"""
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'remove_gpu',
|
||||
'--gpu-id', 'gpu_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'gpu_id' in data or 'status' in data
|
||||
|
||||
def test_edge_gpu_scan_gpus(self, runner, mock_config, edge_available):
|
||||
"""Test scanning for available GPUs"""
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'scan_gpus'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'gpus' in data or 'scan_results' in data
|
||||
|
||||
def test_edge_gpu_gpu_metrics(self, runner, mock_config, edge_available):
|
||||
"""Test getting GPU metrics"""
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'gpu_metrics',
|
||||
'--gpu-id', 'gpu_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'metrics' in data or 'gpu_id' in data
|
||||
|
||||
# Database operations
|
||||
def test_edge_database_init_db(self, runner, mock_config, edge_available):
|
||||
"""Test initializing a database"""
|
||||
result = runner.invoke(edge, [
|
||||
'database', 'init_db',
|
||||
'--db-name', 'test_db'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'db_id' in data or 'status' in data
|
||||
|
||||
def test_edge_database_list_dbs(self, runner, mock_config, edge_available):
|
||||
"""Test listing databases"""
|
||||
result = runner.invoke(edge, [
|
||||
'database', 'list_dbs'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'databases' in data or isinstance(data, list)
|
||||
|
||||
def test_edge_database_get_db(self, runner, mock_config, edge_available):
|
||||
"""Test getting database info"""
|
||||
result = runner.invoke(edge, [
|
||||
'database', 'get_db',
|
||||
'--db-id', 'db_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'db_id' in data or 'status' in data
|
||||
|
||||
def test_edge_database_delete_db(self, runner, mock_config, edge_available):
|
||||
"""Test deleting a database"""
|
||||
result = runner.invoke(edge, [
|
||||
'database', 'delete_db',
|
||||
'--db-id', 'db_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'db_id' in data or 'status' in data
|
||||
|
||||
def test_edge_database_sync_db(self, runner, mock_config, edge_available):
|
||||
"""Test syncing a database"""
|
||||
result = runner.invoke(edge, [
|
||||
'database', 'sync_db',
|
||||
'--db-id', 'db_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'db_id' in data or 'sync_status' in data
|
||||
|
||||
# Serve operations
|
||||
def test_edge_serve_submit_request(self, runner, mock_config, edge_available):
|
||||
"""Test submitting a serve request"""
|
||||
result = runner.invoke(edge, [
|
||||
'serve', 'submit_request',
|
||||
'--request-type', 'compute',
|
||||
'--parameters', '{"gpu_count": 2}'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'request_id' in data or 'status' in data
|
||||
|
||||
def test_edge_serve_list_requests(self, runner, mock_config, edge_available):
|
||||
"""Test listing serve requests"""
|
||||
result = runner.invoke(edge, [
|
||||
'serve', 'list_requests'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'requests' in data or isinstance(data, list)
|
||||
|
||||
def test_edge_serve_get_request(self, runner, mock_config, edge_available):
|
||||
"""Test getting serve request info"""
|
||||
result = runner.invoke(edge, [
|
||||
'serve', 'get_request',
|
||||
'--request-id', 'req_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'request_id' in data or 'status' in data
|
||||
|
||||
def test_edge_serve_cancel_request(self, runner, mock_config, edge_available):
|
||||
"""Test cancelling a serve request"""
|
||||
result = runner.invoke(edge, [
|
||||
'serve', 'cancel_request',
|
||||
'--request-id', 'req_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'request_id' in data or 'status' in data
|
||||
|
||||
def test_edge_serve_get_result(self, runner, mock_config, edge_available):
|
||||
"""Test getting serve request result"""
|
||||
result = runner.invoke(edge, [
|
||||
'serve', 'get_result',
|
||||
'--request-id', 'req_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'result' in data or 'request_id' in data
|
||||
|
||||
# Metrics operations
|
||||
def test_edge_metrics_record(self, runner, mock_config, edge_available):
|
||||
"""Test recording a metric"""
|
||||
result = runner.invoke(edge, [
|
||||
'metrics', 'record',
|
||||
'--metric-name', 'test_metric',
|
||||
'--value', '100'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'metric_id' in data or 'status' in data
|
||||
|
||||
def test_edge_metrics_list_metrics(self, runner, mock_config, edge_available):
|
||||
"""Test listing metrics"""
|
||||
result = runner.invoke(edge, [
|
||||
'metrics', 'list_metrics'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'metrics' in data or isinstance(data, list)
|
||||
|
||||
def test_edge_metrics_get_metric(self, runner, mock_config, edge_available):
|
||||
"""Test getting a specific metric"""
|
||||
result = runner.invoke(edge, [
|
||||
'metrics', 'get_metric',
|
||||
'--metric-id', 'metric_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'metric_id' in data or 'value' in data
|
||||
|
||||
def test_edge_metrics_delete_metric(self, runner, mock_config, edge_available):
|
||||
"""Test deleting a metric"""
|
||||
result = runner.invoke(edge, [
|
||||
'metrics', 'delete_metric',
|
||||
'--metric-id', 'metric_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'metric_id' in data or 'status' in data
|
||||
|
||||
# Error handling tests
|
||||
def test_edge_island_leave_nonexistent(self, runner, mock_config):
|
||||
"""Test leaving non-existent island"""
|
||||
result = runner.invoke(edge, [
|
||||
'island', 'leave',
|
||||
'--island-id', 'nonexistent_island'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should handle gracefully
|
||||
assert result.exit_code != 0 or 'not found' in result.output.lower()
|
||||
|
||||
def test_edge_gpu_get_nonexistent(self, runner, mock_config):
|
||||
"""Test getting non-existent GPU"""
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'get_gpu',
|
||||
'--gpu-id', 'nonexistent_gpu'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should handle gracefully
|
||||
assert result.exit_code != 0 or 'not found' in result.output.lower()
|
||||
|
||||
def test_edge_api_error_handling(self, runner, mock_config):
|
||||
"""Test edge command handles edge-api errors gracefully"""
|
||||
# Use invalid edge URL to trigger error
|
||||
mock_config.coordinator_url = "http://invalid:9999"
|
||||
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'list_gpus'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should either fail gracefully or skip with appropriate message
|
||||
assert result.exit_code != 0 or 'error' in result.output.lower() or 'unavailable' in result.output.lower()
|
||||
|
||||
# Output format tests
|
||||
def test_edge_gpu_list_table_format(self, runner, mock_config, edge_available):
|
||||
"""Test GPU list in table format"""
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'list_gpus'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'GPU' in result.output or 'gpus' in result.output.lower()
|
||||
|
||||
def test_edge_database_list_table_format(self, runner, mock_config, edge_available):
|
||||
"""Test database list in table format"""
|
||||
result = runner.invoke(edge, [
|
||||
'database', 'list_dbs'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Database' in result.output or 'databases' in result.output.lower()
|
||||
|
||||
@patch('aitbc_cli.commands.edge.get_config')
|
||||
@patch('aitbc_cli.commands.edge.AITBCHTTPClient')
|
||||
def test_edge_gpu_list_via_edge_api(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test GPU listing via edge-api"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.get.return_value = {
|
||||
"gpus": [
|
||||
{"id": "gpu_1", "type": "NVIDIA", "memory": 16},
|
||||
{"id": "gpu_2", "type": "NVIDIA", "memory": 32}
|
||||
]
|
||||
}
|
||||
|
||||
result = runner.invoke(edge, [
|
||||
'gpu', 'list_gpus'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
450
tests/cli/test_resource.py
Normal file
450
tests/cli/test_resource.py
Normal file
@@ -0,0 +1,450 @@
|
||||
"""Integration tests for resource CLI commands
|
||||
|
||||
These tests require coordinator-api running and validate resource allocation,
|
||||
utilization tracking, and API interactions with actual service calls.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import httpx
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from aitbc_cli.commands.resource import resource
|
||||
from aitbc import AITBCHTTPClient, NetworkError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
config = Mock()
|
||||
config.coordinator_url = "http://127.0.0.1:18000"
|
||||
config.api_key = "test_api_key"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_http_client():
|
||||
"""Mock HTTP client for coordinator-api"""
|
||||
client = MagicMock(spec=AITBCHTTPClient)
|
||||
return client
|
||||
|
||||
|
||||
class TestResourceCommands:
|
||||
"""Integration tests for resource commands with coordinator-api"""
|
||||
|
||||
@pytest.fixture
|
||||
def coordinator_available(self):
|
||||
"""Skip test if coordinator-api is not running"""
|
||||
try:
|
||||
response = httpx.get("http://127.0.0.1:18000/health", timeout=2)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
except Exception:
|
||||
pytest.skip("coordinator-api not running at http://127.0.0.1:18000")
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_status_all(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test getting status of all resources"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.get.return_value = {
|
||||
"resources": [
|
||||
{"id": "res_1", "type": "gpu", "status": "allocated"},
|
||||
{"id": "res_2", "type": "cpu", "status": "available"}
|
||||
]
|
||||
}
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_client.get.assert_called_once_with("/api/v1/resources/status")
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_status_specific(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test getting status of specific resource"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.get.return_value = {
|
||||
"id": "res_123",
|
||||
"type": "gpu",
|
||||
"status": "allocated",
|
||||
"efficiency": "85.5%"
|
||||
}
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'status',
|
||||
'--resource-id', 'res_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_client.get.assert_called_once_with("/api/v1/resources/res_123/status")
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_deallocate(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test deallocating a resource"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.post.return_value = {
|
||||
"resource_id": "res_123",
|
||||
"status": "deallocated",
|
||||
"timestamp": "2026-05-27T08:30:00Z"
|
||||
}
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'deallocate', 'res_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_client.post.assert_called_once_with("/api/v1/resources/res_123/deallocate")
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_deallocate_force(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test force deallocating a resource without confirmation"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.post.return_value = {
|
||||
"resource_id": "res_123",
|
||||
"status": "deallocated"
|
||||
}
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'deallocate', 'res_123',
|
||||
'--force'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_client.post.assert_called_once_with("/api/v1/resources/res_123/deallocate")
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_status_network_error(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test resource status with network error"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.get.side_effect = NetworkError("Connection refused")
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "Network error" in result.output
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_deallocate_network_error(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test resource deallocation with network error"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.post.side_effect = NetworkError("Connection refused")
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'deallocate', 'res_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "Network error" in result.output
|
||||
|
||||
def test_resource_allocate_experimental_warning(self, runner, mock_config):
|
||||
"""Test that allocate command shows experimental warning without --mock"""
|
||||
result = runner.invoke(resource, [
|
||||
'allocate',
|
||||
'--resource-type', 'gpu',
|
||||
'--quantity', '4'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
# Should fail with experimental warning
|
||||
assert result.exit_code != 0
|
||||
assert "EXPERIMENTAL" in result.output
|
||||
assert "--mock" in result.output
|
||||
|
||||
def test_resource_list_experimental_warning(self, runner, mock_config):
|
||||
"""Test that list command shows experimental warning without --mock"""
|
||||
result = runner.invoke(resource, [
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
# Should fail with experimental warning
|
||||
assert result.exit_code != 0
|
||||
assert "EXPERIMENTAL" in result.output
|
||||
assert "--mock" in result.output
|
||||
|
||||
def test_resource_release_experimental_warning(self, runner, mock_config):
|
||||
"""Test that release command shows experimental warning without --mock"""
|
||||
result = runner.invoke(resource, [
|
||||
'release', 'res_123'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
# Should fail with experimental warning
|
||||
assert result.exit_code != 0
|
||||
assert "EXPERIMENTAL" in result.output
|
||||
assert "--mock" in result.output
|
||||
|
||||
def test_resource_utilization_experimental_warning(self, runner, mock_config):
|
||||
"""Test that utilization command shows experimental warning without --mock"""
|
||||
result = runner.invoke(resource, [
|
||||
'utilization'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
# Should fail with experimental warning
|
||||
assert result.exit_code != 0
|
||||
assert "EXPERIMENTAL" in result.output
|
||||
assert "--mock" in result.output
|
||||
|
||||
def test_resource_optimize_experimental_warning(self, runner, mock_config):
|
||||
"""Test that optimize command shows experimental warning without --mock"""
|
||||
result = runner.invoke(resource, [
|
||||
'optimize'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
# Should fail with experimental warning
|
||||
assert result.exit_code != 0
|
||||
assert "EXPERIMENTAL" in result.output
|
||||
assert "--mock" in result.output
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_status_table_format(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test resource status in table format"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.get.return_value = {
|
||||
"resources": [
|
||||
{"id": "res_1", "type": "gpu", "status": "allocated"}
|
||||
]
|
||||
}
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Resource Status" in result.output
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_deallocate_with_confirmation(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test resource deallocation with user confirmation"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.post.return_value = {
|
||||
"resource_id": "res_123",
|
||||
"status": "deallocated"
|
||||
}
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'deallocate', 'res_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'}, input='y\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_client.post.assert_called_once_with("/api/v1/resources/res_123/deallocate")
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_deallocate_cancelled(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test resource deallocation cancelled by user"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'deallocate', 'res_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'}, input='n\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Should not call post if cancelled
|
||||
mock_client.post.assert_not_called()
|
||||
|
||||
@patch('aitbc_cli.commands.resource.get_config')
|
||||
@patch('aitbc_cli.commands.resource.AITBCHTTPClient')
|
||||
def test_resource_status_empty_response(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test resource status with empty response"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.get.return_value = {}
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_client.get.assert_called_once_with("/api/v1/resources/status")
|
||||
|
||||
def test_resource_status_with_coordinator_api(self, runner, mock_config, coordinator_available):
|
||||
"""Test resource status with actual coordinator-api call"""
|
||||
result = runner.invoke(resource, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'resources' in data or isinstance(data, list)
|
||||
|
||||
def test_resource_deallocate_with_coordinator_api(self, runner, mock_config, coordinator_available):
|
||||
"""Test resource deallocation with actual coordinator-api call"""
|
||||
result = runner.invoke(resource, [
|
||||
'deallocate', 'test_res_123',
|
||||
'--force'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'resource_id' in data or 'status' in data
|
||||
|
||||
def test_resource_allocate_with_mock(self, runner, mock_config):
|
||||
"""Test resource allocation with mock flag"""
|
||||
result = runner.invoke(resource, [
|
||||
'allocate',
|
||||
'--resource-type', 'gpu',
|
||||
'--quantity', '4',
|
||||
'--mock'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'resource_id' in data or 'allocation_id' in data
|
||||
|
||||
def test_resource_list_with_mock(self, runner, mock_config):
|
||||
"""Test resource listing with mock flag"""
|
||||
result = runner.invoke(resource, [
|
||||
'list',
|
||||
'--mock'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'resources' in data or isinstance(data, list)
|
||||
|
||||
def test_resource_release_with_mock(self, runner, mock_config):
|
||||
"""Test resource release with mock flag"""
|
||||
result = runner.invoke(resource, [
|
||||
'release', 'test_res_123',
|
||||
'--mock'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'resource_id' in data or 'status' in data
|
||||
|
||||
def test_resource_utilization_with_mock(self, runner, mock_config):
|
||||
"""Test resource utilization with mock flag"""
|
||||
result = runner.invoke(resource, [
|
||||
'utilization',
|
||||
'--mock'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'utilization' in data or 'metrics' in data
|
||||
|
||||
def test_resource_optimize_with_mock(self, runner, mock_config):
|
||||
"""Test resource optimization with mock flag"""
|
||||
result = runner.invoke(resource, [
|
||||
'optimize',
|
||||
'--mock'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'optimization' in data or 'recommendations' in data
|
||||
|
||||
def test_resource_allocate_with_parameters(self, runner, mock_config):
|
||||
"""Test resource allocation with custom parameters"""
|
||||
result = runner.invoke(resource, [
|
||||
'allocate',
|
||||
'--resource-type', 'gpu',
|
||||
'--quantity', '8',
|
||||
'--min-memory', '32',
|
||||
'--mock'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'resource_id' in data or 'allocation_id' in data
|
||||
|
||||
def test_resource_status_filter_by_type(self, runner, mock_config, coordinator_available):
|
||||
"""Test resource status filtered by resource type"""
|
||||
result = runner.invoke(resource, [
|
||||
'status',
|
||||
'--resource-type', 'gpu'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
# Verify filtering was applied
|
||||
if 'resources' in data and isinstance(data['resources'], list):
|
||||
for res in data['resources']:
|
||||
assert res.get('type') == 'gpu' or 'type' not in res
|
||||
|
||||
def test_resource_api_error_handling(self, runner, mock_config):
|
||||
"""Test resource command handles coordinator-api errors gracefully"""
|
||||
# Use invalid coordinator URL to trigger error
|
||||
mock_config.coordinator_url = "http://invalid:9999"
|
||||
|
||||
result = runner.invoke(resource, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should either fail gracefully or skip with appropriate message
|
||||
assert result.exit_code != 0 or 'error' in result.output.lower() or 'unavailable' in result.output.lower()
|
||||
337
tests/cli/test_simulate_integration.py
Normal file
337
tests/cli/test_simulate_integration.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""Integration tests for simulate CLI commands
|
||||
|
||||
These tests require coordinator-api running and validate simulation operations
|
||||
including blockchain, wallets, price, network, and ai-jobs simulations with actual service calls.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import httpx
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from aitbc_cli.commands.simulate import simulate
|
||||
from aitbc import AITBCHTTPClient, NetworkError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
config = Mock()
|
||||
config.coordinator_url = "http://127.0.0.1:18000"
|
||||
config.api_key = "test_api_key"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_http_client():
|
||||
"""Mock HTTP client for coordinator-api"""
|
||||
client = MagicMock(spec=AITBCHTTPClient)
|
||||
return client
|
||||
|
||||
|
||||
class TestSimulateCommandsIntegration:
|
||||
"""Integration tests for simulate commands with coordinator-api"""
|
||||
|
||||
@pytest.fixture
|
||||
def coordinator_available(self):
|
||||
"""Skip test if coordinator-api is not running"""
|
||||
try:
|
||||
response = httpx.get("http://127.0.0.1:18000/health", timeout=2)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
except Exception:
|
||||
pytest.skip("coordinator-api not running at http://127.0.0.1:18000")
|
||||
|
||||
def test_simulate_blockchain(self, runner, mock_config, coordinator_available):
|
||||
"""Test blockchain simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'blockchain',
|
||||
'--blocks', '10',
|
||||
'--transactions', '50'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'blocks' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_wallets(self, runner, mock_config, coordinator_available):
|
||||
"""Test wallet simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'wallets',
|
||||
'--count', '5',
|
||||
'--balance', '1000'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'wallets' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_price(self, runner, mock_config, coordinator_available):
|
||||
"""Test price simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'price',
|
||||
'--days', '30',
|
||||
'--volatility', '0.1'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'prices' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_network(self, runner, mock_config, coordinator_available):
|
||||
"""Test network simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'network',
|
||||
'--nodes', '10',
|
||||
'--latency', '50'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'network' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_ai_jobs(self, runner, mock_config, coordinator_available):
|
||||
"""Test AI jobs simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'ai-jobs',
|
||||
'--jobs', '20',
|
||||
'--duration', '300'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'jobs' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_run(self, runner, mock_config, coordinator_available):
|
||||
"""Test running a simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'run',
|
||||
'--type', 'blockchain',
|
||||
'--duration', '60'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'simulation_id' in data or 'status' in data
|
||||
|
||||
def test_simulate_status(self, runner, mock_config, coordinator_available):
|
||||
"""Test getting simulation status"""
|
||||
# First run a simulation
|
||||
run_result = runner.invoke(simulate, [
|
||||
'run',
|
||||
'--type', 'blockchain'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert run_result.exit_code == 0
|
||||
run_data = json.loads(run_result.output)
|
||||
sim_id = run_data.get('simulation_id')
|
||||
|
||||
if sim_id:
|
||||
# Get status
|
||||
status_result = runner.invoke(simulate, [
|
||||
'status', sim_id
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert status_result.exit_code == 0
|
||||
status_data = json.loads(status_result.output)
|
||||
assert 'status' in status_data
|
||||
|
||||
def test_simulate_result(self, runner, mock_config, coordinator_available):
|
||||
"""Test getting simulation results"""
|
||||
# First run a simulation
|
||||
run_result = runner.invoke(simulate, [
|
||||
'run',
|
||||
'--type', 'wallets'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert run_result.exit_code == 0
|
||||
run_data = json.loads(run_result.output)
|
||||
sim_id = run_data.get('simulation_id')
|
||||
|
||||
if sim_id:
|
||||
# Get results
|
||||
result_result = runner.invoke(simulate, [
|
||||
'result', sim_id
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result_result.exit_code == 0
|
||||
result_data = json.loads(result_result.output)
|
||||
assert 'results' in result_data or 'data' in result_data
|
||||
|
||||
def test_simulate_blockchain_with_params(self, runner, mock_config, coordinator_available):
|
||||
"""Test blockchain simulation with custom parameters"""
|
||||
result = runner.invoke(simulate, [
|
||||
'blockchain',
|
||||
'--blocks', '100',
|
||||
'--transactions', '500',
|
||||
'--difficulty', '5'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'blocks' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_wallets_with_distribution(self, runner, mock_config, coordinator_available):
|
||||
"""Test wallet simulation with balance distribution"""
|
||||
result = runner.invoke(simulate, [
|
||||
'wallets',
|
||||
'--count', '10',
|
||||
'--distribution', 'exponential'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'wallets' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_price_with_trend(self, runner, mock_config, coordinator_available):
|
||||
"""Test price simulation with trend"""
|
||||
result = runner.invoke(simulate, [
|
||||
'price',
|
||||
'--days', '90',
|
||||
'--trend', 'bullish',
|
||||
'--volatility', '0.15'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'prices' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_network_with_topology(self, runner, mock_config, coordinator_available):
|
||||
"""Test network simulation with custom topology"""
|
||||
result = runner.invoke(simulate, [
|
||||
'network',
|
||||
'--nodes', '20',
|
||||
'--topology', 'mesh',
|
||||
'--latency', '100'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'network' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_ai_jobs_with_gpu(self, runner, mock_config, coordinator_available):
|
||||
"""Test AI jobs simulation with GPU requirements"""
|
||||
result = runner.invoke(simulate, [
|
||||
'ai-jobs',
|
||||
'--jobs', '30',
|
||||
'--gpu-required',
|
||||
'--duration', '600'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'jobs' in data or 'simulation_id' in data
|
||||
|
||||
def test_simulate_run_async(self, runner, mock_config, coordinator_available):
|
||||
"""Test running simulation in async mode"""
|
||||
result = runner.invoke(simulate, [
|
||||
'run',
|
||||
'--type', 'network',
|
||||
'--async',
|
||||
'--duration', '120'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'simulation_id' in data
|
||||
assert data.get('status') in ['started', 'running', 'pending']
|
||||
|
||||
def test_simulate_status_nonexistent(self, runner, mock_config):
|
||||
"""Test getting status of non-existent simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'status', 'sim_nonexistent_12345'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should handle gracefully
|
||||
assert result.exit_code != 0 or 'not found' in result.output.lower()
|
||||
|
||||
def test_simulate_result_nonexistent(self, runner, mock_config):
|
||||
"""Test getting results of non-existent simulation"""
|
||||
result = runner.invoke(simulate, [
|
||||
'result', 'sim_nonexistent_12345'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should handle gracefully
|
||||
assert result.exit_code != 0 or 'not found' in result.output.lower()
|
||||
|
||||
def test_simulate_multiple_concurrent(self, runner, mock_config, coordinator_available):
|
||||
"""Test running multiple concurrent simulations"""
|
||||
sim_ids = []
|
||||
|
||||
for i in range(3):
|
||||
result = runner.invoke(simulate, [
|
||||
'run',
|
||||
'--type', 'blockchain',
|
||||
'--async'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
sim_id = data.get('simulation_id')
|
||||
if sim_id:
|
||||
sim_ids.append(sim_id)
|
||||
|
||||
# Verify we got multiple simulation IDs
|
||||
assert len(sim_ids) > 0
|
||||
|
||||
def test_simulate_api_error_handling(self, runner, mock_config):
|
||||
"""Test simulate command handles coordinator-api errors gracefully"""
|
||||
# Use invalid coordinator URL to trigger error
|
||||
mock_config.coordinator_url = "http://invalid:9999"
|
||||
|
||||
result = runner.invoke(simulate, [
|
||||
'blockchain'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should either fail gracefully or skip with appropriate message
|
||||
assert result.exit_code != 0 or 'error' in result.output.lower() or 'unavailable' in result.output.lower()
|
||||
|
||||
@patch('aitbc_cli.commands.simulate.get_config')
|
||||
@patch('aitbc_cli.commands.simulate.AITBCHTTPClient')
|
||||
def test_simulate_blockchain_via_coordinator_api(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test blockchain simulation via coordinator-api"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.post.return_value = {
|
||||
"simulation_id": "sim_123",
|
||||
"status": "started",
|
||||
"blocks": 10
|
||||
}
|
||||
|
||||
result = runner.invoke(simulate, [
|
||||
'blockchain',
|
||||
'--blocks', '10'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Verify API was called (if simulate command uses coordinator-api)
|
||||
|
||||
def test_simulate_output_formats(self, runner, mock_config, coordinator_available):
|
||||
"""Test simulation output in different formats"""
|
||||
# JSON format
|
||||
result_json = runner.invoke(simulate, [
|
||||
'blockchain',
|
||||
'--blocks', '5'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result_json.exit_code == 0
|
||||
json.loads(result_json.output) # Should be valid JSON
|
||||
|
||||
# Table format
|
||||
result_table = runner.invoke(simulate, [
|
||||
'blockchain',
|
||||
'--blocks', '5'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result_table.exit_code == 0
|
||||
380
tests/cli/test_workflow.py
Normal file
380
tests/cli/test_workflow.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""Integration tests for workflow CLI commands
|
||||
|
||||
These tests require coordinator-api running and validate workflow execution,
|
||||
status tracking, and API interactions with actual service calls.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import time
|
||||
import httpx
|
||||
from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from aitbc_cli.commands.workflow import workflow
|
||||
from aitbc import AITBCHTTPClient, NetworkError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
config = Mock()
|
||||
config.coordinator_url = "http://127.0.0.1:18000"
|
||||
config.api_key = "test_api_key"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_http_client():
|
||||
"""Mock HTTP client for coordinator-api"""
|
||||
client = MagicMock(spec=AITBCHTTPClient)
|
||||
return client
|
||||
|
||||
|
||||
class TestWorkflowCommands:
|
||||
"""Integration tests for workflow commands with coordinator-api"""
|
||||
|
||||
@pytest.fixture
|
||||
def coordinator_available(self):
|
||||
"""Skip test if coordinator-api is not running"""
|
||||
try:
|
||||
response = httpx.get("http://127.0.0.1:18000/health", timeout=2)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
except Exception:
|
||||
pytest.skip("coordinator-api not running at http://127.0.0.1:18000")
|
||||
|
||||
def test_workflow_run_basic(self, runner, mock_config):
|
||||
"""Test running a basic workflow"""
|
||||
result = runner.invoke(workflow, [
|
||||
'run', 'test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'test_workflow' in result.output
|
||||
assert 'Running' in result.output
|
||||
|
||||
def test_workflow_run_with_config(self, runner, mock_config, tmp_path):
|
||||
"""Test running workflow with config file"""
|
||||
config_file = tmp_path / "workflow_config.yaml"
|
||||
config_file.write_text("param1: value1\nparam2: value2")
|
||||
|
||||
result = runner.invoke(workflow, [
|
||||
'run', 'test_workflow',
|
||||
'--config', str(config_file)
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'test_workflow' in result.output
|
||||
assert str(config_file) in result.output
|
||||
|
||||
def test_workflow_run_dry_run(self, runner, mock_config):
|
||||
"""Test workflow dry run mode"""
|
||||
result = runner.invoke(workflow, [
|
||||
'run', 'test_workflow',
|
||||
'--dry-run'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Dry run' in result.output
|
||||
assert 'without making changes' in result.output
|
||||
|
||||
def test_workflow_list(self, runner, mock_config):
|
||||
"""Test listing available workflows"""
|
||||
result = runner.invoke(workflow, [
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'workflows' in data or isinstance(data, list)
|
||||
|
||||
# If it's a list, check structure
|
||||
if isinstance(data, list):
|
||||
assert len(data) > 0
|
||||
assert 'name' in data[0]
|
||||
assert 'status' in data[0]
|
||||
|
||||
def test_workflow_list_table_format(self, runner, mock_config):
|
||||
"""Test listing workflows in table format"""
|
||||
result = runner.invoke(workflow, [
|
||||
'list',
|
||||
'--format', 'table'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Available workflows' in result.output
|
||||
|
||||
def test_workflow_status(self, runner, mock_config):
|
||||
"""Test getting workflow status"""
|
||||
result = runner.invoke(workflow, [
|
||||
'status', 'test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'test_workflow' in result.output
|
||||
assert 'Status' in result.output
|
||||
|
||||
def test_workflow_stop(self, runner, mock_config):
|
||||
"""Test stopping a workflow"""
|
||||
result = runner.invoke(workflow, [
|
||||
'stop', 'test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'test_workflow' in result.output
|
||||
assert 'Stop' in result.output
|
||||
|
||||
@patch('aitbc_cli.commands.workflow.get_config')
|
||||
@patch('aitbc_cli.commands.workflow.AITBCHTTPClient')
|
||||
def test_workflow_run_via_coordinator_api(self, mock_http_client_class, mock_get_config, runner):
|
||||
"""Test workflow execution via coordinator-api"""
|
||||
# Setup mocks
|
||||
mock_config = Mock()
|
||||
mock_config.coordinator_url = "http://127.0.0.1:18000"
|
||||
mock_get_config.return_value = mock_config
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_http_client_class.return_value = mock_client
|
||||
mock_client.post.return_value = {
|
||||
"workflow_id": "wf_123",
|
||||
"status": "started",
|
||||
"execution_id": "exec_456"
|
||||
}
|
||||
|
||||
result = runner.invoke(workflow, [
|
||||
'run', 'api_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Verify API was called (if workflow command uses coordinator-api)
|
||||
# This depends on actual implementation
|
||||
|
||||
def test_workflow_execution_id_generation(self, runner, mock_config):
|
||||
"""Test that workflow execution generates unique IDs"""
|
||||
result1 = runner.invoke(workflow, [
|
||||
'run', 'test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
time.sleep(0.1) # Small delay to ensure different timestamp
|
||||
|
||||
result2 = runner.invoke(workflow, [
|
||||
'run', 'test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result1.exit_code == 0
|
||||
assert result2.exit_code == 0
|
||||
|
||||
# Extract execution IDs from output
|
||||
import re
|
||||
id_pattern = r'wf_exec_\d+'
|
||||
ids1 = re.findall(id_pattern, result1.output)
|
||||
ids2 = re.findall(id_pattern, result2.output)
|
||||
|
||||
if ids1 and ids2:
|
||||
assert ids1[0] != ids2[0], "Execution IDs should be unique"
|
||||
|
||||
def test_workflow_nonexistent_status(self, runner, mock_config):
|
||||
"""Test getting status of non-existent workflow"""
|
||||
result = runner.invoke(workflow, [
|
||||
'status', 'nonexistent_workflow_xyz'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Should return status even for non-existent workflows
|
||||
assert 'nonexistent_workflow_xyz' in result.output
|
||||
|
||||
def test_workflow_stop_nonexistent(self, runner, mock_config):
|
||||
"""Test stopping non-existent workflow"""
|
||||
result = runner.invoke(workflow, [
|
||||
'stop', 'nonexistent_workflow_xyz'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Should attempt to stop even if not running
|
||||
assert 'nonexistent_workflow_xyz' in result.output
|
||||
|
||||
def test_workflow_with_special_characters(self, runner, mock_config):
|
||||
"""Test workflow names with special characters"""
|
||||
special_names = [
|
||||
'workflow-with-dashes',
|
||||
'workflow_with_underscores',
|
||||
'workflow.with.dots',
|
||||
'WorkflowWithCamelCase'
|
||||
]
|
||||
|
||||
for name in special_names:
|
||||
result = runner.invoke(workflow, [
|
||||
'run', name
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert name in result.output
|
||||
|
||||
def test_workflow_list_filters(self, runner, mock_config):
|
||||
"""Test workflow listing with potential filters"""
|
||||
result = runner.invoke(workflow, [
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
|
||||
# Verify expected workflow types are present
|
||||
if isinstance(data, list):
|
||||
workflow_names = [w['name'] for w in data]
|
||||
# Check for known workflow types from implementation
|
||||
expected_types = ['gpu-marketplace', 'ai-job-processing', 'mining-optimization']
|
||||
for expected in expected_types:
|
||||
if expected in workflow_names:
|
||||
assert True # Found expected workflow
|
||||
break
|
||||
|
||||
def test_workflow_status_output_format(self, runner, mock_config):
|
||||
"""Test workflow status in different output formats"""
|
||||
# Table format
|
||||
result_table = runner.invoke(workflow, [
|
||||
'status', 'test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result_table.exit_code == 0
|
||||
|
||||
# JSON format
|
||||
result_json = runner.invoke(workflow, [
|
||||
'status', 'test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result_json.exit_code == 0
|
||||
# Should be parseable as JSON or contain status info
|
||||
|
||||
def test_workflow_run_with_coordinator_api(self, runner, mock_config, coordinator_available):
|
||||
"""Test workflow execution with actual coordinator-api call"""
|
||||
result = runner.invoke(workflow, [
|
||||
'run', 'test_integration_workflow',
|
||||
'--async'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'workflow_id' in data or 'execution_id' in data
|
||||
assert data.get('status') in ['started', 'running', 'pending']
|
||||
|
||||
def test_workflow_list_with_coordinator_api(self, runner, mock_config, coordinator_available):
|
||||
"""Test listing workflows from coordinator-api"""
|
||||
result = runner.invoke(workflow, [
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'workflows' in data or isinstance(data, list)
|
||||
|
||||
# Validate workflow structure
|
||||
if isinstance(data, list):
|
||||
for workflow in data:
|
||||
assert 'name' in workflow
|
||||
assert 'status' in workflow
|
||||
|
||||
def test_workflow_status_with_coordinator_api(self, runner, mock_config, coordinator_available):
|
||||
"""Test getting workflow status from coordinator-api"""
|
||||
# First run a workflow
|
||||
run_result = runner.invoke(workflow, [
|
||||
'run', 'status_test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert run_result.exit_code == 0
|
||||
run_data = json.loads(run_result.output)
|
||||
workflow_id = run_data.get('workflow_id') or run_data.get('execution_id')
|
||||
|
||||
if workflow_id:
|
||||
# Get status
|
||||
status_result = runner.invoke(workflow, [
|
||||
'status', workflow_id
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert status_result.exit_code == 0
|
||||
status_data = json.loads(status_result.output)
|
||||
assert 'status' in status_data
|
||||
assert workflow_id in str(status_data)
|
||||
|
||||
def test_workflow_stop_with_coordinator_api(self, runner, mock_config, coordinator_available):
|
||||
"""Test stopping workflow via coordinator-api"""
|
||||
# Run a workflow
|
||||
run_result = runner.invoke(workflow, [
|
||||
'run', 'stop_test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert run_result.exit_code == 0
|
||||
run_data = json.loads(run_result.output)
|
||||
workflow_id = run_data.get('workflow_id') or run_data.get('execution_id')
|
||||
|
||||
if workflow_id:
|
||||
# Stop the workflow
|
||||
stop_result = runner.invoke(workflow, [
|
||||
'stop', workflow_id
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert stop_result.exit_code == 0
|
||||
stop_data = json.loads(stop_result.output)
|
||||
assert stop_data.get('status') in ['stopped', 'stopping', 'cancelled']
|
||||
|
||||
def test_workflow_run_with_parameters(self, runner, mock_config, coordinator_available):
|
||||
"""Test workflow execution with custom parameters"""
|
||||
result = runner.invoke(workflow, [
|
||||
'run', 'param_test_workflow',
|
||||
'--param', 'gpu_count=4',
|
||||
'--param', 'timeout=300'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'workflow_id' in data or 'execution_id' in data
|
||||
|
||||
def test_workflow_execution_tracking(self, runner, mock_config, coordinator_available):
|
||||
"""Test tracking workflow execution over time"""
|
||||
# Start workflow
|
||||
run_result = runner.invoke(workflow, [
|
||||
'run', 'tracking_test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert run_result.exit_code == 0
|
||||
run_data = json.loads(run_result.output)
|
||||
workflow_id = run_data.get('workflow_id') or run_data.get('execution_id')
|
||||
|
||||
if workflow_id:
|
||||
# Check status immediately
|
||||
status1 = runner.invoke(workflow, [
|
||||
'status', workflow_id
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert status1.exit_code == 0
|
||||
|
||||
# Wait and check status again
|
||||
time.sleep(1)
|
||||
|
||||
status2 = runner.invoke(workflow, [
|
||||
'status', workflow_id
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert status2.exit_code == 0
|
||||
status2_data = json.loads(status2.output)
|
||||
assert 'status' in status2_data
|
||||
|
||||
def test_workflow_api_error_handling(self, runner, mock_config):
|
||||
"""Test workflow command handles coordinator-api errors gracefully"""
|
||||
# Use invalid coordinator URL to trigger error
|
||||
mock_config.coordinator_url = "http://invalid:9999"
|
||||
|
||||
result = runner.invoke(workflow, [
|
||||
'run', 'error_test_workflow'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Should either fail gracefully or skip with appropriate message
|
||||
# The exact behavior depends on implementation
|
||||
assert result.exit_code != 0 or 'error' in result.output.lower() or 'unavailable' in result.output.lower()
|
||||
Reference in New Issue
Block a user