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

- 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:
aitbc
2026-05-27 09:16:23 +02:00
parent 7f71d8a6c7
commit 2acb5ccc49
17 changed files with 5403 additions and 20 deletions

View 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()

View 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
View 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()

View 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
View 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()