Remove outdated GPU marketplace endpoint and fix staking service logic
- Remove duplicate `/marketplace/gpu/{gpu_id}` endpoint from marketplace_gpu.py
- Remove marketplace_gpu router inclusion from main.py (already included elsewhere)
- Fix staking service staker_count logic to check existing stakes before increment/decrement
- Add minimum stake amount validation (100 AITBC)
- Add proper error handling for stake not found cases
- Fix staking pool update to commit and refresh after modifications
- Update CLI send_transaction to use chain
This commit is contained in:
342
tests/contracts/staking_test_plan.md
Normal file
342
tests/contracts/staking_test_plan.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# AgentStaking Contract + Staking Service Test Plan
|
||||
|
||||
## Overview
|
||||
Test plan for the AgentStaking smart contract (`/opt/aitbc/contracts/contracts/AgentStaking.sol`) and the Python staking service (`/opt/aitbc/apps/coordinator-api/src/app/services/staking_service.py`).
|
||||
|
||||
## Test Environment
|
||||
- Contract: AgentStaking.sol (Solidity ^0.8.19)
|
||||
- Service: staking_service.py (Python)
|
||||
- Database: SQLModel with AgentStake, AgentMetrics, StakingPool models
|
||||
- Blockchain: ait-testnet (multi-chain mode)
|
||||
|
||||
## Test Categories
|
||||
|
||||
### 1. Contract Unit Tests (Solidity)
|
||||
|
||||
#### 1.1 Staking Operations
|
||||
- **Test 1.1.1**: Create stake with valid parameters
|
||||
- Input: valid agent wallet, amount within limits, valid lock period
|
||||
- Expected: Stake created with correct APY, status ACTIVE
|
||||
- Verify: StakeCreated event emitted, stake stored correctly
|
||||
|
||||
- **Test 1.1.2**: Create stake with insufficient balance
|
||||
- Input: amount > user balance
|
||||
- Expected: Transaction reverts with "Insufficient balance"
|
||||
- Verify: No stake created, no tokens transferred
|
||||
|
||||
- **Test 1.1.3**: Create stake with invalid amount (below minimum)
|
||||
- Input: amount < 100 AITBC
|
||||
- Expected: Transaction reverts with "Invalid stake amount"
|
||||
- Verify: No stake created
|
||||
|
||||
- **Test 1.1.4**: Create stake with invalid amount (above maximum)
|
||||
- Input: amount > 100,000 AITBC
|
||||
- Expected: Transaction reverts with "Invalid stake amount"
|
||||
- Verify: No stake created
|
||||
|
||||
- **Test 1.1.5**: Create stake on unsupported agent
|
||||
- Input: agent wallet not in supported agents list
|
||||
- Expected: Transaction reverts with "Agent not supported"
|
||||
- Verify: No stake created
|
||||
|
||||
- **Test 1.1.6**: Create stake with invalid lock period (too short)
|
||||
- Input: lock period < 1 day
|
||||
- Expected: Transaction reverts with "Lock period too short"
|
||||
- Verify: No stake created
|
||||
|
||||
- **Test 1.1.7**: Create stake with invalid lock period (too long)
|
||||
- Input: lock period > 365 days
|
||||
- Expected: Transaction reverts with "Lock period too long"
|
||||
- Verify: No stake created
|
||||
|
||||
#### 1.2 APY Calculation
|
||||
- **Test 1.2.1**: APY calculation for Bronze tier (1x multiplier)
|
||||
- Input: agent tier = BRONZE, lock period = 30 days
|
||||
- Expected: APY = 5% * 1.0 * 1.1 = 5.5%
|
||||
|
||||
- **Test 1.2.2**: APY calculation for Diamond tier (3x multiplier)
|
||||
- Input: agent tier = DIAMOND, lock period = 365 days
|
||||
- Expected: APY = 5% * 3.0 * 2.0 = 30% (capped at 20%)
|
||||
|
||||
- **Test 1.2.3**: APY capping at maximum
|
||||
- Input: high tier + long lock period
|
||||
- Expected: APY capped at 20%
|
||||
|
||||
#### 1.3 Adding to Stake
|
||||
- **Test 1.3.1**: Add to active stake
|
||||
- Input: valid stake ID, additional amount
|
||||
- Expected: Stake amount updated, APY recalculated
|
||||
- Verify: StakeUpdated event emitted
|
||||
|
||||
- **Test 1.3.2**: Add to non-existent stake
|
||||
- Input: invalid stake ID
|
||||
- Expected: Transaction reverts with "Stake does not exist"
|
||||
|
||||
- **Test 1.3.3**: Add to unbonding stake
|
||||
- Input: stake with UNBONDING status
|
||||
- Expected: Transaction reverts with "Stake not active"
|
||||
|
||||
#### 1.4 Unbonding Operations
|
||||
- **Test 1.4.1**: Initiate unbonding after lock period
|
||||
- Input: stake ID, lock period elapsed
|
||||
- Expected: Stake status = UNBONDING, rewards calculated
|
||||
- Verify: StakeUnbonded event emitted
|
||||
|
||||
- **Test 1.4.2**: Initiate unbonding before lock period
|
||||
- Input: stake ID, lock period not elapsed
|
||||
- Expected: Transaction reverts with "Lock period not ended"
|
||||
|
||||
- **Test 1.4.3**: Complete unbonding after unbonding period
|
||||
- Input: stake ID, unbonding period elapsed
|
||||
- Expected: Stake status = COMPLETED, tokens returned + rewards
|
||||
- Verify: StakeCompleted event emitted
|
||||
|
||||
- **Test 1.4.4**: Complete unbonding with early penalty
|
||||
- Input: stake ID, completed within 30 days
|
||||
- Expected: 10% penalty applied
|
||||
- Verify: Penalty deducted from returned amount
|
||||
|
||||
#### 1.5 Reward Distribution
|
||||
- **Test 1.5.1**: Distribute earnings to stakers
|
||||
- Input: agent wallet, total earnings
|
||||
- Expected: Earnings distributed proportionally, platform fee deducted
|
||||
- Verify: PoolRewardsDistributed event emitted
|
||||
|
||||
- **Test 1.5.2**: Distribute with no stakers
|
||||
- Input: agent wallet with no stakers
|
||||
- Expected: Transaction reverts with "No stakers in pool"
|
||||
|
||||
#### 1.6 Agent Performance Updates
|
||||
- **Test 1.6.1**: Update agent performance (tier upgrade)
|
||||
- Input: agent wallet, high accuracy, successful submission
|
||||
- Expected: Agent tier upgraded, all active stakes APY updated
|
||||
- Verify: AgentTierUpdated event emitted
|
||||
|
||||
- **Test 1.6.2**: Update agent performance (tier downgrade)
|
||||
- Input: agent wallet, low accuracy, failed submission
|
||||
- Expected: Agent tier downgraded, all active stakes APY updated
|
||||
- Verify: AgentTierUpdated event emitted
|
||||
|
||||
#### 1.7 Configuration Updates
|
||||
- **Test 1.7.1**: Update base APY
|
||||
- Input: new base APY (within limits)
|
||||
- Expected: baseAPY updated
|
||||
- Verify: New APY applies to future stakes
|
||||
|
||||
- **Test 1.7.2**: Update APY with invalid values
|
||||
- Input: base APY > max APY
|
||||
- Expected: Transaction reverts with "Base APY cannot exceed max APY"
|
||||
|
||||
### 2. Service Integration Tests (Python)
|
||||
|
||||
#### 2.1 Staking Service Operations
|
||||
- **Test 2.1.1**: Create stake via service
|
||||
- Input: valid staker address, agent wallet, amount, lock period
|
||||
- Expected: Stake created in database, agent metrics updated
|
||||
- Verify: Database contains stake, agent total_staked increased
|
||||
|
||||
- **Test 2.1.2**: Get stake by ID
|
||||
- Input: valid stake ID
|
||||
- Expected: Stake details returned
|
||||
- Verify: All stake fields present
|
||||
|
||||
- **Test 2.1.3**: Get user stakes with filters
|
||||
- Input: user address, status filter, tier filter
|
||||
- Expected: Filtered list of stakes returned
|
||||
- Verify: Pagination works correctly
|
||||
|
||||
- **Test 2.1.4**: Add to stake via service
|
||||
- Input: stake ID, additional amount
|
||||
- Expected: Stake amount updated in database
|
||||
- Verify: Agent metrics updated
|
||||
|
||||
- **Test 2.1.5**: Calculate rewards
|
||||
- Input: stake ID
|
||||
- Expected: Current reward amount calculated
|
||||
- Verify: Calculation matches expected formula
|
||||
|
||||
#### 2.2 Agent Metrics Management
|
||||
- **Test 2.2.1**: Get agent metrics
|
||||
- Input: agent wallet address
|
||||
- Expected: Agent metrics returned
|
||||
- Verify: All metrics fields present
|
||||
|
||||
- **Test 2.2.2**: Update agent performance
|
||||
- Input: agent wallet, accuracy, successful flag
|
||||
- Expected: Metrics updated, tier recalculated if needed
|
||||
- Verify: Success rate calculated correctly
|
||||
|
||||
- **Test 2.2.3**: Calculate agent tier
|
||||
- Input: agent metrics with high performance
|
||||
- Expected: DIAMOND tier returned
|
||||
- Verify: Tier calculation formula correct
|
||||
|
||||
#### 2.3 Staking Pool Operations
|
||||
- **Test 2.3.1**: Get staking pool
|
||||
- Input: agent wallet
|
||||
- Expected: Pool details returned
|
||||
- Verify: Pool statistics accurate
|
||||
|
||||
- **Test 2.3.2**: Update staking pool (add stake)
|
||||
- Input: agent wallet, staker address, amount
|
||||
- Expected: Pool total_staked increased
|
||||
- Verify: Staker added to active_stakers if new
|
||||
|
||||
- **Test 2.3.3**: Update staking pool (remove stake)
|
||||
- Input: agent wallet, staker address, amount
|
||||
- Expected: Pool total_staked decreased
|
||||
- Verify: Staker removed if no shares remaining
|
||||
|
||||
#### 2.4 Earnings Distribution
|
||||
- **Test 2.4.1**: Distribute earnings
|
||||
- Input: agent wallet, total earnings
|
||||
- Expected: Earnings distributed proportionally to stakers
|
||||
- Verify: Platform fee deducted correctly
|
||||
|
||||
- **Test 2.4.2**: Get supported agents
|
||||
- Input: tier filter, pagination
|
||||
- Expected: Filtered list of agents returned
|
||||
- Verify: Current APY calculated for each agent
|
||||
|
||||
#### 2.5 Statistics and Reporting
|
||||
- **Test 2.5.1**: Get staking stats (daily)
|
||||
- Input: period = "daily"
|
||||
- Expected: Statistics for last 24 hours
|
||||
- Verify: Total staked, active stakes, unique stakers accurate
|
||||
|
||||
- **Test 2.5.2**: Get staking stats (weekly)
|
||||
- Input: period = "weekly"
|
||||
- Expected: Statistics for last 7 days
|
||||
- Verify: Tier distribution calculated correctly
|
||||
|
||||
- **Test 2.5.3**: Get leaderboard (total_staked)
|
||||
- Input: period = "weekly", metric = "total_staked"
|
||||
- Expected: Agents ranked by total staked
|
||||
- Verify: Ranking order correct
|
||||
|
||||
- **Test 2.5.4**: Get leaderboard (total_rewards)
|
||||
- Input: period = "weekly", metric = "total_rewards"
|
||||
- Expected: Agents ranked by rewards distributed
|
||||
- Verify: Ranking order correct
|
||||
|
||||
- **Test 2.5.5**: Get user rewards
|
||||
- Input: user address, period
|
||||
- Expected: User's staking rewards summary
|
||||
- Verify: Total rewards and average APY calculated correctly
|
||||
|
||||
#### 2.6 Risk Assessment
|
||||
- **Test 2.6.1**: Get risk assessment (low risk)
|
||||
- Input: high-performing agent
|
||||
- Expected: Low risk level
|
||||
- Verify: Risk factors calculated correctly
|
||||
|
||||
- **Test 2.6.2**: Get risk assessment (high risk)
|
||||
- Input: low-performing new agent
|
||||
- Expected: High risk level
|
||||
- Verify: Recommendations provided
|
||||
|
||||
### 3. Integration Tests (Contract + Service)
|
||||
|
||||
#### 3.1 End-to-End Staking Flow
|
||||
- **Test 3.1.1**: Complete staking lifecycle
|
||||
- Steps:
|
||||
1. Register agent in contract
|
||||
2. Create stake via service
|
||||
3. Wait for lock period
|
||||
4. Initiate unbonding
|
||||
5. Complete unbonding
|
||||
- Expected: Full cycle completes successfully
|
||||
- Verify: Tokens returned with correct rewards
|
||||
|
||||
#### 3.2 Performance Tier Updates
|
||||
- **Test 3.2.1**: Agent tier upgrade affects staking APY
|
||||
- Steps:
|
||||
1. Create stake on Bronze tier agent
|
||||
2. Update agent performance to DIAMOND tier
|
||||
3. Verify APY updated for active stakes
|
||||
- Expected: APY increased for all active stakes
|
||||
|
||||
#### 3.3 Reward Distribution Flow
|
||||
- **Test 3.3.1**: Agent earnings distributed to stakers
|
||||
- Steps:
|
||||
1. Multiple users stake on agent
|
||||
2. Agent earns rewards
|
||||
3. Distribute earnings via contract
|
||||
4. Verify service reflects updated rewards
|
||||
- Expected: All stakers receive proportional rewards
|
||||
|
||||
### 4. Edge Cases and Error Handling
|
||||
|
||||
#### 4.1 Reentrancy Protection
|
||||
- **Test 4.1.1**: Reentrancy attack on stake creation
|
||||
- Input: Malicious contract attempting reentrancy
|
||||
- Expected: Transaction reverts
|
||||
- Verify: ReentrancyGuard prevents attack
|
||||
|
||||
#### 4.2 Pause Functionality
|
||||
- **Test 4.2.1**: Pause contract during emergency
|
||||
- Input: Call pause() as owner
|
||||
- Expected: All state-changing functions revert
|
||||
- Verify: Only owner can pause/unpause
|
||||
|
||||
#### 4.3 Integer Overflow/Underflow
|
||||
- **Test 4.3.1**: Large stake amounts
|
||||
- Input: Maximum stake amount
|
||||
- Expected: No overflow, calculations correct
|
||||
- Verify: Solidity 0.8.19 has built-in overflow protection
|
||||
|
||||
#### 4.4 Database Transaction Rollback
|
||||
- **Test 4.4.1**: Service operation failure triggers rollback
|
||||
- Input: Invalid data causing service error
|
||||
- Expected: Database transaction rolled back
|
||||
- Verify: No partial state changes
|
||||
|
||||
### 5. Test Execution Priority
|
||||
|
||||
#### High Priority (Critical Path)
|
||||
1. Test 1.1.1: Create stake with valid parameters
|
||||
2. Test 1.4.1: Initiate unbonding after lock period
|
||||
3. Test 1.4.3: Complete unbonding after unbonding period
|
||||
4. Test 2.1.1: Create stake via service
|
||||
5. Test 3.1.1: Complete staking lifecycle
|
||||
|
||||
#### Medium Priority (Important Features)
|
||||
6. Test 1.2.1-1.2.3: APY calculation
|
||||
7. Test 1.5.1: Distribute earnings to stakers
|
||||
8. Test 1.6.1: Update agent performance
|
||||
9. Test 2.2.1-2.2.3: Agent metrics management
|
||||
10. Test 2.4.1: Distribute earnings via service
|
||||
|
||||
#### Low Priority (Edge Cases)
|
||||
11. Test 4.1-4.4: Security and edge cases
|
||||
12. Test 2.5.1-2.5.5: Statistics and reporting
|
||||
13. Test 2.6.1-2.6.2: Risk assessment
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Required Test Accounts
|
||||
- Owner account (for configuration updates)
|
||||
- Staker accounts (multiple for testing distribution)
|
||||
- Agent wallets (for different performance tiers)
|
||||
- Malicious account (for security tests)
|
||||
|
||||
### Required Test Data
|
||||
- AITBC token balances (pre-funded)
|
||||
- Agent performance data (accuracy, success rates)
|
||||
- Staking pool data (initial state)
|
||||
- Historical performance data
|
||||
|
||||
## Success Criteria
|
||||
- All high priority tests pass
|
||||
- At least 80% of medium priority tests pass
|
||||
- No critical security vulnerabilities found
|
||||
- Performance within acceptable limits
|
||||
- Gas optimization verified for contract functions
|
||||
|
||||
## Test Deliverables
|
||||
1. Test suite for AgentStaking contract (Solidity/Hardhat)
|
||||
2. Test suite for staking_service (Python/pytest)
|
||||
3. Integration test suite
|
||||
4. Test execution report
|
||||
5. Bug report with severity classification
|
||||
6. Performance analysis report
|
||||
332
tests/fixtures/staking_fixtures.py
vendored
Normal file
332
tests/fixtures/staking_fixtures.py
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Shared fixtures for staking tests
|
||||
Reusable fixtures for service and integration tests to avoid duplication
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
# Add paths for imports
|
||||
sys.path.insert(0, "/opt/aitbc/apps/coordinator-api/src")
|
||||
|
||||
from app.domain.bounty import (
|
||||
AgentStake, AgentMetrics, StakingPool,
|
||||
StakeStatus, PerformanceTier
|
||||
)
|
||||
from app.services.staking_service import StakingService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""Create in-memory SQLite database for testing"""
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
SQLModel.metadata.create_all(engine)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staking_service(db_session):
|
||||
"""Create StakingService instance with test session"""
|
||||
return StakingService(db_session)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_wallet():
|
||||
"""Default test agent wallet address"""
|
||||
return "0x1234567890123456789012345678901234567890"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staker_address():
|
||||
"""Default test staker address"""
|
||||
return "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_metrics(agent_wallet):
|
||||
"""Create test agent metrics with GOLD tier"""
|
||||
return AgentMetrics(
|
||||
agent_wallet=agent_wallet,
|
||||
total_submissions=10,
|
||||
successful_submissions=9,
|
||||
average_accuracy=95.0,
|
||||
current_tier=PerformanceTier.GOLD,
|
||||
tier_score=85.0,
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
last_update_time=datetime.utcnow()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_metrics_bronze(agent_wallet):
|
||||
"""Create test agent metrics with BRONZE tier"""
|
||||
return AgentMetrics(
|
||||
agent_wallet=agent_wallet,
|
||||
total_submissions=5,
|
||||
successful_submissions=4,
|
||||
average_accuracy=80.0,
|
||||
current_tier=PerformanceTier.BRONZE,
|
||||
tier_score=60.0,
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
last_update_time=datetime.utcnow()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_metrics_diamond(agent_wallet):
|
||||
"""Create test agent metrics with DIAMOND tier"""
|
||||
return AgentMetrics(
|
||||
agent_wallet=agent_wallet,
|
||||
total_submissions=50,
|
||||
successful_submissions=48,
|
||||
average_accuracy=98.0,
|
||||
current_tier=PerformanceTier.DIAMOND,
|
||||
tier_score=95.0,
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
last_update_time=datetime.utcnow()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staking_pool(db_session, agent_metrics):
|
||||
"""Create test staking pool"""
|
||||
pool = StakingPool(
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
total_staked=0.0,
|
||||
total_rewards=0.0,
|
||||
pool_apy=5.0,
|
||||
staker_count=0,
|
||||
active_stakers=[],
|
||||
last_distribution_time=datetime.utcnow(),
|
||||
distribution_frequency=1
|
||||
)
|
||||
db_session.add(pool)
|
||||
db_session.commit()
|
||||
db_session.refresh(pool)
|
||||
return pool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stake_data():
|
||||
"""Default stake creation data"""
|
||||
return {
|
||||
"amount": 1000.0,
|
||||
"lock_period": 30,
|
||||
"auto_compound": False
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def large_stake_data():
|
||||
"""Large stake creation data"""
|
||||
return {
|
||||
"amount": 50000.0,
|
||||
"lock_period": 90,
|
||||
"auto_compound": True
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def small_stake_data():
|
||||
"""Small stake creation data"""
|
||||
return {
|
||||
"amount": 100.0,
|
||||
"lock_period": 7,
|
||||
"auto_compound": False
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def invalid_stake_data():
|
||||
"""Invalid stake creation data (below minimum)"""
|
||||
return {
|
||||
"amount": 50.0,
|
||||
"lock_period": 30,
|
||||
"auto_compound": False
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def created_stake(staking_service, agent_metrics, staker_address, stake_data):
|
||||
"""Create a stake for testing"""
|
||||
return staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=stake_data["amount"],
|
||||
lock_period=stake_data["lock_period"],
|
||||
auto_compound=stake_data["auto_compound"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def active_stake(db_session, agent_wallet, staker_address):
|
||||
"""Create an active stake directly in database"""
|
||||
stake = AgentStake(
|
||||
stake_id="stake_test_001",
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
start_time=datetime.utcnow(),
|
||||
end_time=datetime.utcnow() + timedelta(days=30),
|
||||
status=StakeStatus.ACTIVE,
|
||||
accumulated_rewards=0.0,
|
||||
last_reward_time=datetime.utcnow(),
|
||||
current_apy=8.25,
|
||||
agent_tier=PerformanceTier.GOLD,
|
||||
performance_multiplier=1.5,
|
||||
auto_compound=False
|
||||
)
|
||||
db_session.add(stake)
|
||||
db_session.commit()
|
||||
db_session.refresh(stake)
|
||||
return stake
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unbonding_stake(db_session, agent_wallet, staker_address):
|
||||
"""Create an unbonding stake directly in database"""
|
||||
stake = AgentStake(
|
||||
stake_id="stake_test_002",
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
start_time=datetime.utcnow() - timedelta(days=35),
|
||||
end_time=datetime.utcnow() - timedelta(days=5),
|
||||
status=StakeStatus.UNBONDING,
|
||||
accumulated_rewards=50.0,
|
||||
last_reward_time=datetime.utcnow() - timedelta(days=5),
|
||||
current_apy=8.25,
|
||||
agent_tier=PerformanceTier.GOLD,
|
||||
performance_multiplier=1.5,
|
||||
auto_compound=False,
|
||||
unbonding_time=datetime.utcnow() - timedelta(days=5)
|
||||
)
|
||||
db_session.add(stake)
|
||||
db_session.commit()
|
||||
db_session.refresh(stake)
|
||||
return stake
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def completed_stake(db_session, agent_wallet, staker_address):
|
||||
"""Create a completed stake directly in database"""
|
||||
stake = AgentStake(
|
||||
stake_id="stake_test_003",
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
start_time=datetime.utcnow() - timedelta(days=70),
|
||||
end_time=datetime.utcnow() - timedelta(days=40),
|
||||
status=StakeStatus.COMPLETED,
|
||||
accumulated_rewards=100.0,
|
||||
last_reward_time=datetime.utcnow() - timedelta(days=40),
|
||||
current_apy=8.25,
|
||||
agent_tier=PerformanceTier.GOLD,
|
||||
performance_multiplier=1.5,
|
||||
auto_compound=False,
|
||||
unbonding_time=datetime.utcnow() - timedelta(days=40)
|
||||
)
|
||||
db_session.add(stake)
|
||||
db_session.commit()
|
||||
db_session.refresh(stake)
|
||||
return stake
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multiple_stakes(db_session, agent_wallet, staker_address):
|
||||
"""Create multiple stakes for testing"""
|
||||
stakes = []
|
||||
|
||||
# Stake 1: Active, 30-day lock
|
||||
stake1 = AgentStake(
|
||||
stake_id="stake_test_001",
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
start_time=datetime.utcnow(),
|
||||
end_time=datetime.utcnow() + timedelta(days=30),
|
||||
status=StakeStatus.ACTIVE,
|
||||
accumulated_rewards=0.0,
|
||||
last_reward_time=datetime.utcnow(),
|
||||
current_apy=8.25,
|
||||
agent_tier=PerformanceTier.GOLD,
|
||||
performance_multiplier=1.5,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Stake 2: Active, 90-day lock with auto-compound
|
||||
stake2 = AgentStake(
|
||||
stake_id="stake_test_002",
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=2000.0,
|
||||
lock_period=90,
|
||||
start_time=datetime.utcnow(),
|
||||
end_time=datetime.utcnow() + timedelta(days=90),
|
||||
status=StakeStatus.ACTIVE,
|
||||
accumulated_rewards=0.0,
|
||||
last_reward_time=datetime.utcnow(),
|
||||
current_apy=10.0,
|
||||
agent_tier=PerformanceTier.GOLD,
|
||||
performance_multiplier=1.5,
|
||||
auto_compound=True
|
||||
)
|
||||
|
||||
db_session.add_all([stake1, stake2])
|
||||
db_session.commit()
|
||||
|
||||
for stake in [stake1, stake2]:
|
||||
db_session.refresh(stake)
|
||||
stakes.append(stake)
|
||||
|
||||
return stakes
|
||||
|
||||
|
||||
def calculate_expected_apy(base_apy=5.0, tier_multiplier=1.0, lock_multiplier=1.0):
|
||||
"""Calculate expected APY based on parameters"""
|
||||
apy = base_apy * tier_multiplier * lock_multiplier
|
||||
return min(apy, 20.0) # Cap at 20%
|
||||
|
||||
|
||||
def get_tier_multiplier(tier):
|
||||
"""Get tier multiplier for APY calculation"""
|
||||
multipliers = {
|
||||
PerformanceTier.BRONZE: 1.0,
|
||||
PerformanceTier.SILVER: 1.25,
|
||||
PerformanceTier.GOLD: 1.5,
|
||||
PerformanceTier.PLATINUM: 2.0,
|
||||
PerformanceTier.DIAMOND: 3.0
|
||||
}
|
||||
return multipliers.get(tier, 1.0)
|
||||
|
||||
|
||||
def get_lock_multiplier(lock_period_days):
|
||||
"""Get lock period multiplier for APY calculation"""
|
||||
if lock_period_days >= 365:
|
||||
return 2.0
|
||||
elif lock_period_days >= 90:
|
||||
return 1.5
|
||||
elif lock_period_days >= 30:
|
||||
return 1.1
|
||||
else:
|
||||
return 1.0
|
||||
337
tests/integration/test_staking_lifecycle.py
Normal file
337
tests/integration/test_staking_lifecycle.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Staking Lifecycle Integration Tests
|
||||
Test 3.1.1: Complete staking lifecycle integration test
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
sys.path.insert(0, "/opt/aitbc/apps/coordinator-api/src")
|
||||
sys.path.insert(0, "/opt/aitbc/contracts")
|
||||
|
||||
# Import after path setup
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.domain.bounty import AgentStake, AgentMetrics, StakingPool, StakeStatus, PerformanceTier
|
||||
from app.services.staking_service import StakingService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""Create SQLite in-memory database for testing"""
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
session = SessionLocal()
|
||||
|
||||
yield session
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staking_service(db_session):
|
||||
"""Create staking service instance"""
|
||||
return StakingService(db_session)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_wallet():
|
||||
"""Test agent wallet address"""
|
||||
return "0x1234567890123456789012345678901234567890"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staker_address():
|
||||
"""Test staker address"""
|
||||
return "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_metrics(db_session, agent_wallet):
|
||||
"""Create test agent metrics"""
|
||||
metrics = AgentMetrics(
|
||||
agent_wallet=agent_wallet,
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
average_accuracy=95.0,
|
||||
total_submissions=10,
|
||||
successful_submissions=9,
|
||||
success_rate=90.0,
|
||||
current_tier=PerformanceTier.GOLD,
|
||||
tier_score=80.0
|
||||
)
|
||||
db_session.add(metrics)
|
||||
db_session.commit()
|
||||
db_session.refresh(metrics)
|
||||
return metrics
|
||||
|
||||
|
||||
class TestStakingLifecycle:
|
||||
"""Test 3.1.1: Complete staking lifecycle integration test"""
|
||||
|
||||
async def test_complete_staking_lifecycle(
|
||||
self,
|
||||
staking_service,
|
||||
agent_metrics,
|
||||
staker_address,
|
||||
agent_wallet
|
||||
):
|
||||
"""Test complete staking lifecycle: create stake → unbond → complete"""
|
||||
|
||||
# Step 1: Create stake
|
||||
print("\n=== Step 1: Creating stake ===")
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
assert stake is not None
|
||||
assert stake.status == StakeStatus.ACTIVE
|
||||
assert stake.amount == 1000.0
|
||||
print(f"✓ Stake created: {stake.stake_id}")
|
||||
|
||||
# Verify agent metrics updated
|
||||
updated_metrics = await staking_service.get_agent_metrics(agent_wallet)
|
||||
assert updated_metrics.total_staked == 1000.0
|
||||
assert updated_metrics.staker_count == 1
|
||||
print(f"✓ Agent metrics updated: total_staked={updated_metrics.total_staked}")
|
||||
|
||||
# Verify staking pool updated
|
||||
updated_pool = await staking_service.get_staking_pool(agent_wallet)
|
||||
assert updated_pool.total_staked == 1000.0
|
||||
print(f"✓ Staking pool updated: total_staked={updated_pool.total_staked}")
|
||||
|
||||
# Step 2: Calculate rewards
|
||||
print("\n=== Step 2: Calculating rewards ===")
|
||||
rewards = await staking_service.calculate_rewards(stake.stake_id)
|
||||
print(f"✓ Rewards calculated: {rewards}")
|
||||
|
||||
# Step 3: Simulate time passing (lock period elapsed)
|
||||
print("\n=== Step 3: Simulating lock period ===")
|
||||
# In a real scenario, this would be actual time passing
|
||||
# For testing, we'll just verify the logic works
|
||||
stake.end_time = datetime.utcnow() - timedelta(days=1) # Lock period ended
|
||||
staking_service.session.commit()
|
||||
print(f"✓ Lock period simulated as ended")
|
||||
|
||||
# Step 4: Initiate unbonding
|
||||
print("\n=== Step 4: Initiating unbonding ===")
|
||||
unbonded_stake = await staking_service.unbond_stake(stake.stake_id)
|
||||
assert unbonded_stake.status == StakeStatus.UNBONDING
|
||||
print(f"✓ Unbonding initiated: status={unbonded_stake.status}")
|
||||
|
||||
# Step 5: Simulate unbonding period
|
||||
print("\n=== Step 5: Simulating unbonding period ===")
|
||||
unbonded_stake.unbonding_time = datetime.utcnow() - timedelta(days=8) # 8 days ago
|
||||
staking_service.session.commit()
|
||||
print(f"✓ Unbonding period simulated as ended")
|
||||
|
||||
# Step 6: Complete unbonding
|
||||
print("\n=== Step 6: Completing unbonding ===")
|
||||
result = await staking_service.complete_unbonding(stake.stake_id)
|
||||
|
||||
assert result is not None
|
||||
assert "total_amount" in result
|
||||
assert "total_rewards" in result
|
||||
assert "penalty" in result
|
||||
print(f"✓ Unbonding completed:")
|
||||
print(f" - Total amount: {result['total_amount']}")
|
||||
print(f" - Total rewards: {result['total_rewards']}")
|
||||
print(f" - Penalty: {result['penalty']}")
|
||||
|
||||
# Verify stake status
|
||||
completed_stake = await staking_service.get_stake(stake.stake_id)
|
||||
assert completed_stake.status == StakeStatus.COMPLETED
|
||||
print(f"✓ Stake status: {completed_stake.status}")
|
||||
|
||||
# Verify agent metrics updated
|
||||
final_metrics = await staking_service.get_agent_metrics(agent_wallet)
|
||||
assert final_metrics.total_staked == 0.0
|
||||
assert final_metrics.staker_count == 0
|
||||
print(f"✓ Agent metrics reset: total_staked={final_metrics.total_staked}")
|
||||
|
||||
# Verify staking pool updated
|
||||
final_pool = await staking_service.get_staking_pool(agent_wallet)
|
||||
assert final_pool.total_staked == 0.0
|
||||
assert staker_address not in final_pool.active_stakers
|
||||
print(f"✓ Staking pool reset: total_staked={final_pool.total_staked}")
|
||||
|
||||
print("\n=== Complete staking lifecycle test PASSED ===")
|
||||
|
||||
async def test_stake_accumulation_over_time(
|
||||
self,
|
||||
staking_service,
|
||||
agent_metrics,
|
||||
staker_address,
|
||||
agent_wallet
|
||||
):
|
||||
"""Test rewards accumulation over time"""
|
||||
|
||||
# Create stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Calculate initial rewards
|
||||
initial_rewards = await staking_service.calculate_rewards(stake.stake_id)
|
||||
print(f"Initial rewards: {initial_rewards}")
|
||||
|
||||
# Simulate time passing by updating last_reward_time
|
||||
stake.last_reward_time = datetime.utcnow() - timedelta(days=10)
|
||||
staking_service.session.commit()
|
||||
|
||||
# Calculate rewards after 10 days
|
||||
rewards_after_10_days = await staking_service.calculate_rewards(stake.stake_id)
|
||||
print(f"Rewards after 10 days: {rewards_after_10_days}")
|
||||
|
||||
# Rewards should have increased
|
||||
assert rewards_after_10_days >= initial_rewards
|
||||
print("✓ Rewards accumulated over time")
|
||||
|
||||
async def test_multiple_stakes_same_agent(
|
||||
self,
|
||||
staking_service,
|
||||
agent_metrics,
|
||||
staker_address,
|
||||
agent_wallet
|
||||
):
|
||||
"""Test multiple stakes on the same agent"""
|
||||
|
||||
# Create first stake
|
||||
stake1 = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=500.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Create second stake
|
||||
stake2 = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_wallet,
|
||||
amount=1500.0,
|
||||
lock_period=60,
|
||||
auto_compound=True
|
||||
)
|
||||
|
||||
# Verify both stakes exist
|
||||
assert stake1.stake_id != stake2.stake_id
|
||||
assert stake1.amount == 500.0
|
||||
assert stake2.amount == 1500.0
|
||||
assert stake2.auto_compound == True
|
||||
|
||||
# Verify agent metrics
|
||||
metrics = await staking_service.get_agent_metrics(agent_wallet)
|
||||
assert metrics.total_staked == 2000.0
|
||||
assert metrics.staker_count == 1 # Same staker
|
||||
|
||||
# Verify staking pool
|
||||
pool = await staking_service.get_staking_pool(agent_wallet)
|
||||
assert pool.total_staked == 2000.0
|
||||
|
||||
print("✓ Multiple stakes on same agent created successfully")
|
||||
|
||||
async def test_stake_with_different_tiers(
|
||||
self,
|
||||
staking_service,
|
||||
db_session,
|
||||
staker_address,
|
||||
agent_wallet
|
||||
):
|
||||
"""Test stakes on agents with different performance tiers"""
|
||||
|
||||
# Create agents with different tiers
|
||||
bronze_agent = "0x1111111111111111111111111111111111111111"
|
||||
silver_agent = "0x2222222222222222222222222222222222222222"
|
||||
gold_agent = agent_wallet
|
||||
|
||||
bronze_metrics = AgentMetrics(
|
||||
agent_wallet=bronze_agent,
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
average_accuracy=65.0,
|
||||
total_submissions=10,
|
||||
successful_submissions=7,
|
||||
success_rate=70.0,
|
||||
current_tier=PerformanceTier.BRONZE,
|
||||
tier_score=60.0
|
||||
)
|
||||
|
||||
silver_metrics = AgentMetrics(
|
||||
agent_wallet=silver_agent,
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
average_accuracy=85.0,
|
||||
total_submissions=10,
|
||||
successful_submissions=8,
|
||||
success_rate=80.0,
|
||||
current_tier=PerformanceTier.SILVER,
|
||||
tier_score=70.0
|
||||
)
|
||||
|
||||
gold_metrics = AgentMetrics(
|
||||
agent_wallet=gold_agent,
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
average_accuracy=95.0,
|
||||
total_submissions=10,
|
||||
successful_submissions=9,
|
||||
success_rate=90.0,
|
||||
current_tier=PerformanceTier.GOLD,
|
||||
tier_score=80.0
|
||||
)
|
||||
|
||||
db_session.add_all([bronze_metrics, silver_metrics, gold_metrics])
|
||||
db_session.commit()
|
||||
|
||||
# Create stakes on each agent
|
||||
bronze_stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=bronze_agent,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
silver_stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=silver_agent,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
gold_stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=gold_agent,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Verify APY increases with tier
|
||||
assert bronze_stake.current_apy < silver_stake.current_apy
|
||||
assert silver_stake.current_apy < gold_stake.current_apy
|
||||
|
||||
print(f"✓ Bronze tier APY: {bronze_stake.current_apy}%")
|
||||
print(f"✓ Silver tier APY: {silver_stake.current_apy}%")
|
||||
print(f"✓ Gold tier APY: {gold_stake.current_apy}%")
|
||||
print("✓ APY correctly increases with performance tier")
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
import requests
|
||||
import jwt
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, Any
|
||||
|
||||
class TestJWTAuthentication:
|
||||
@@ -32,8 +32,6 @@ class TestJWTAuthentication:
|
||||
assert data["username"] == "admin"
|
||||
assert "expires_at" in data
|
||||
assert data["token_type"] == "Bearer"
|
||||
|
||||
return data["access_token"]
|
||||
|
||||
def test_operator_login(self):
|
||||
"""Test operator user login"""
|
||||
@@ -49,8 +47,6 @@ class TestJWTAuthentication:
|
||||
assert data["role"] == "operator"
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
|
||||
return data["access_token"]
|
||||
|
||||
def test_user_login(self):
|
||||
"""Test regular user login"""
|
||||
@@ -66,8 +62,6 @@ class TestJWTAuthentication:
|
||||
assert data["role"] == "user"
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
|
||||
return data["access_token"]
|
||||
|
||||
def test_invalid_login(self):
|
||||
"""Test login with invalid credentials"""
|
||||
@@ -94,7 +88,12 @@ class TestJWTAuthentication:
|
||||
def test_token_validation(self):
|
||||
"""Test JWT token validation"""
|
||||
# Login to get token
|
||||
token = self.test_admin_login()
|
||||
response = requests.post(
|
||||
f"{self.BASE_URL}/auth/login",
|
||||
json={"username": "admin", "password": "admin123"},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
token = response.json()["access_token"]
|
||||
|
||||
# Validate token
|
||||
response = requests.post(
|
||||
@@ -136,8 +135,8 @@ class TestJWTAuthentication:
|
||||
"user_id": "test_user",
|
||||
"username": "test",
|
||||
"role": "user",
|
||||
"exp": datetime.utcnow() - timedelta(hours=1), # Expired 1 hour ago
|
||||
"iat": datetime.utcnow() - timedelta(hours=2),
|
||||
"exp": datetime.now(timezone.utc) - timedelta(hours=1), # Expired 1 hour ago
|
||||
"iat": datetime.now(timezone.utc) - timedelta(hours=2),
|
||||
"type": "access"
|
||||
}
|
||||
|
||||
@@ -326,13 +325,26 @@ class TestAPIKeyManagement:
|
||||
assert "permissions" in data
|
||||
assert "created_at" in data
|
||||
assert len(data["api_key"]) > 30 # Should be a long secure key
|
||||
|
||||
return data["api_key"]
|
||||
|
||||
def test_validate_api_key(self):
|
||||
"""Test API key validation"""
|
||||
# Generate API key first
|
||||
api_key = self.test_generate_api_key()
|
||||
# Login as admin and generate API key
|
||||
response = requests.post(
|
||||
f"{self.BASE_URL}/auth/login",
|
||||
json={"username": "admin", "password": "admin123"},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
token = response.json()["access_token"]
|
||||
|
||||
response = requests.post(
|
||||
f"{self.BASE_URL}/auth/api-key/generate?user_id=test_user_validate",
|
||||
json=["agent:view", "task:view"],
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
api_key = response.json()["api_key"]
|
||||
|
||||
# Validate API key (use query parameter)
|
||||
response = requests.post(
|
||||
|
||||
@@ -6,8 +6,7 @@ Tests Prometheus metrics, alerting, and SLA monitoring systems
|
||||
import pytest
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, Any
|
||||
|
||||
class TestPrometheusMetrics:
|
||||
@@ -501,10 +500,10 @@ class TestMonitoringIntegration:
|
||||
assert uptime_diff < 1.0, f"Uptime difference {uptime_diff} exceeds tolerance of 1.0 second"
|
||||
|
||||
# Check timestamps are recent
|
||||
summary_time = datetime.fromisoformat(summary["timestamp"].replace('Z', '+00:00'))
|
||||
system_time = datetime.fromisoformat(system["timestamp"].replace('Z', '+00:00'))
|
||||
summary_time = datetime.fromisoformat(summary["timestamp"].replace('Z', '+00:00')).replace(tzinfo=timezone.utc)
|
||||
system_time = datetime.fromisoformat(system["timestamp"].replace('Z', '+00:00')).replace(tzinfo=timezone.utc)
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
assert (now - summary_time).total_seconds() < 60 # Within last minute
|
||||
assert (now - system_time).total_seconds() < 60 # Within last minute
|
||||
|
||||
|
||||
531
tests/services/test_staking_service.py
Normal file
531
tests/services/test_staking_service.py
Normal file
@@ -0,0 +1,531 @@
|
||||
"""
|
||||
Staking Service Tests
|
||||
High-priority tests for staking service functionality
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, "/opt/aitbc/apps/coordinator-api/src")
|
||||
|
||||
from app.domain.bounty import AgentStake, AgentMetrics, StakingPool, StakeStatus, PerformanceTier
|
||||
from app.services.staking_service import StakingService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""Create SQLite in-memory database for testing"""
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
from sqlmodel import SQLModel
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
session = SessionLocal()
|
||||
|
||||
yield session
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staking_service(db_session):
|
||||
"""Create staking service instance"""
|
||||
return StakingService(db_session)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_metrics(db_session):
|
||||
"""Create test agent metrics"""
|
||||
metrics = AgentMetrics(
|
||||
agent_wallet="0x1234567890123456789012345678901234567890",
|
||||
total_staked=0.0,
|
||||
staker_count=0,
|
||||
total_rewards_distributed=0.0,
|
||||
average_accuracy=95.0,
|
||||
total_submissions=10,
|
||||
successful_submissions=9,
|
||||
success_rate=90.0,
|
||||
current_tier=PerformanceTier.GOLD,
|
||||
tier_score=80.0
|
||||
)
|
||||
db_session.add(metrics)
|
||||
db_session.commit()
|
||||
db_session.refresh(metrics)
|
||||
return metrics
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staking_pool(db_session, agent_metrics):
|
||||
"""Create test staking pool"""
|
||||
pool = StakingPool(
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
total_staked=0.0,
|
||||
total_rewards=0.0,
|
||||
pool_apy=5.0,
|
||||
staker_count=0,
|
||||
active_stakers=[],
|
||||
last_distribution_time=datetime.utcnow(),
|
||||
distribution_frequency=1
|
||||
)
|
||||
db_session.add(pool)
|
||||
db_session.commit()
|
||||
db_session.refresh(pool)
|
||||
return pool
|
||||
|
||||
|
||||
class TestStakingService:
|
||||
"""Test 2.1.1: Create stake via service"""
|
||||
|
||||
async def test_create_stake_success(self, staking_service, agent_metrics):
|
||||
"""Test creating a stake with valid parameters"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
amount = 1000.0
|
||||
lock_period = 30
|
||||
auto_compound = False
|
||||
|
||||
# Create stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=amount,
|
||||
lock_period=lock_period,
|
||||
auto_compound=auto_compound
|
||||
)
|
||||
|
||||
# Verify stake created
|
||||
assert stake is not None
|
||||
assert stake.staker_address == staker_address
|
||||
assert stake.agent_wallet == agent_metrics.agent_wallet
|
||||
assert stake.amount == amount
|
||||
assert stake.lock_period == lock_period
|
||||
assert stake.status == StakeStatus.ACTIVE
|
||||
assert stake.auto_compound == auto_compound
|
||||
assert stake.agent_tier == PerformanceTier.GOLD
|
||||
|
||||
# Verify APY calculated
|
||||
# Base APY = 5%, Gold tier multiplier = 1.5, 30-day lock multiplier = 1.1
|
||||
# Expected APY = 5% * 1.5 * 1.1 = 8.25%
|
||||
assert stake.current_apy > 8.0 # Allow small rounding error
|
||||
|
||||
# Verify end time calculated
|
||||
expected_end_time = datetime.utcnow() + timedelta(days=lock_period)
|
||||
time_diff = abs((stake.end_time - expected_end_time).total_seconds())
|
||||
assert time_diff < 60 # Within 1 minute
|
||||
|
||||
# Verify agent metrics updated
|
||||
updated_metrics = await staking_service.get_agent_metrics(agent_metrics.agent_wallet)
|
||||
assert updated_metrics.total_staked == amount
|
||||
assert updated_metrics.staker_count == 1
|
||||
|
||||
async def test_create_stake_unsupported_agent(self, staking_service):
|
||||
"""Test creating stake on unsupported agent"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
unsupported_agent = "0x0000000000000000000000000000000000000000"
|
||||
|
||||
with pytest.raises(ValueError, match="Agent not supported"):
|
||||
await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=unsupported_agent,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
async def test_create_stake_invalid_amount(self, staking_service, agent_metrics):
|
||||
"""Test creating stake with invalid amount (below minimum)"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
with pytest.raises(ValueError, match="Stake amount must be at least 100 AITBC"):
|
||||
await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=50.0, # Below minimum
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
async def test_get_stake(self, staking_service, agent_metrics):
|
||||
"""Test retrieving a stake by ID"""
|
||||
# First create a stake
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
created_stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Retrieve the stake
|
||||
retrieved_stake = await staking_service.get_stake(created_stake.stake_id)
|
||||
|
||||
# Verify stake retrieved
|
||||
assert retrieved_stake is not None
|
||||
assert retrieved_stake.stake_id == created_stake.stake_id
|
||||
assert retrieved_stake.staker_address == staker_address
|
||||
assert retrieved_stake.amount == 1000.0
|
||||
|
||||
async def test_get_stake_not_found(self, staking_service):
|
||||
"""Test retrieving non-existent stake"""
|
||||
with pytest.raises(ValueError, match="Stake not found"):
|
||||
await staking_service.get_stake("nonexistent_stake_id")
|
||||
|
||||
async def test_calculate_apy(self, staking_service, agent_metrics):
|
||||
"""Test APY calculation for different tiers and lock periods"""
|
||||
# Bronze tier, 30 days: 5% * 1.0 * 1.1 = 5.5%
|
||||
apy_bronze_30 = await staking_service.calculate_apy(agent_metrics.agent_wallet, 30)
|
||||
assert apy_bronze_30 > 5.0
|
||||
|
||||
# Gold tier, 90 days: 5% * 1.5 * 1.25 = 9.375%
|
||||
apy_gold_90 = await staking_service.calculate_apy(agent_metrics.agent_wallet, 90)
|
||||
assert apy_gold_90 > 9.0
|
||||
|
||||
# Diamond tier, 365 days: 5% * 3.0 * 2.0 = 30% (capped at 20%)
|
||||
# Update agent to Diamond tier
|
||||
agent_metrics.current_tier = PerformanceTier.DIAMOND
|
||||
staking_service.session.commit()
|
||||
|
||||
apy_diamond_365 = await staking_service.calculate_apy(agent_metrics.agent_wallet, 365)
|
||||
assert apy_diamond_365 == 20.0 # Capped at maximum
|
||||
|
||||
async def test_get_agent_metrics(self, staking_service, agent_metrics):
|
||||
"""Test retrieving agent metrics"""
|
||||
metrics = await staking_service.get_agent_metrics(agent_metrics.agent_wallet)
|
||||
|
||||
assert metrics is not None
|
||||
assert metrics.agent_wallet == agent_metrics.agent_wallet
|
||||
assert metrics.total_staked == 0.0
|
||||
assert metrics.current_tier == PerformanceTier.GOLD
|
||||
assert metrics.average_accuracy == 95.0
|
||||
|
||||
async def test_get_staking_pool(self, staking_service, agent_metrics):
|
||||
"""Test retrieving staking pool after stake creation"""
|
||||
# Create a stake first which will create the pool
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
pool = await staking_service.get_staking_pool(agent_metrics.agent_wallet)
|
||||
|
||||
assert pool is not None
|
||||
assert pool.agent_wallet == agent_metrics.agent_wallet
|
||||
assert pool.total_staked == 1000.0
|
||||
assert pool.pool_apy > 5.0
|
||||
|
||||
async def test_unbond_stake_before_lock_period(self, staking_service, agent_metrics):
|
||||
"""Test unbonding stake before lock period ends should fail"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create a stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Try to unbond immediately (lock period not ended)
|
||||
with pytest.raises(ValueError, match="Lock period has not ended"):
|
||||
await staking_service.unbond_stake(stake.stake_id)
|
||||
|
||||
async def test_unbond_stake_after_lock_period(self, staking_service, agent_metrics):
|
||||
"""Test unbonding stake after lock period ends"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create a stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Simulate lock period ending by updating end_time
|
||||
stake.end_time = datetime.utcnow() - timedelta(days=1)
|
||||
staking_service.session.commit()
|
||||
|
||||
# Unbond the stake
|
||||
unbonded_stake = await staking_service.unbond_stake(stake.stake_id)
|
||||
|
||||
assert unbonded_stake.status == StakeStatus.UNBONDING
|
||||
assert unbonded_stake.unbonding_time is not None
|
||||
|
||||
async def test_complete_unbonding_with_penalty(self, staking_service, agent_metrics):
|
||||
"""Test completing unbonding with early penalty"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create a stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Unbond the stake
|
||||
stake.end_time = datetime.utcnow() - timedelta(days=1)
|
||||
staking_service.session.commit()
|
||||
await staking_service.unbond_stake(stake.stake_id)
|
||||
|
||||
# Complete unbonding within 30 days (should have 10% penalty)
|
||||
result = await staking_service.complete_unbonding(stake.stake_id)
|
||||
|
||||
assert result is not None
|
||||
assert "total_amount" in result
|
||||
assert "penalty" in result
|
||||
# Penalty should be 10% of 1000 = 100, so returned amount should be 900 + rewards
|
||||
assert result["penalty"] == 100.0
|
||||
|
||||
async def test_complete_unbonding_no_penalty(self, staking_service, agent_metrics):
|
||||
"""Test completing unbonding after unbonding period (no penalty)"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create a stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Unbond the stake
|
||||
stake.end_time = datetime.utcnow() - timedelta(days=1)
|
||||
staking_service.session.commit()
|
||||
await staking_service.unbond_stake(stake.stake_id)
|
||||
|
||||
# Set unbonding time to 35 days ago (past 30-day penalty period)
|
||||
stake = await staking_service.get_stake(stake.stake_id)
|
||||
stake.unbonding_time = datetime.utcnow() - timedelta(days=35)
|
||||
staking_service.session.commit()
|
||||
|
||||
# Complete unbonding (no penalty)
|
||||
result = await staking_service.complete_unbonding(stake.stake_id)
|
||||
|
||||
assert result is not None
|
||||
assert "total_amount" in result
|
||||
assert result["penalty"] == 0.0
|
||||
|
||||
async def test_calculate_rewards(self, staking_service, agent_metrics):
|
||||
"""Test reward calculation for active stake"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create a stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Calculate rewards
|
||||
rewards = await staking_service.calculate_rewards(stake.stake_id)
|
||||
|
||||
assert rewards >= 0.0
|
||||
|
||||
async def test_calculate_rewards_unbonding_stake(self, staking_service, agent_metrics):
|
||||
"""Test reward calculation for unbonding stake (should return accumulated)"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create a stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Unbond the stake
|
||||
stake.end_time = datetime.utcnow() - timedelta(days=1)
|
||||
staking_service.session.commit()
|
||||
await staking_service.unbond_stake(stake.stake_id)
|
||||
|
||||
# Calculate rewards (should return accumulated rewards only)
|
||||
rewards = await staking_service.calculate_rewards(stake.stake_id)
|
||||
|
||||
assert rewards == stake.accumulated_rewards
|
||||
|
||||
async def test_create_stake_minimum_amount(self, staking_service, agent_metrics):
|
||||
"""Test creating stake with minimum valid amount"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create stake with exactly minimum amount (100)
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=100.0, # Minimum amount
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
assert stake is not None
|
||||
assert stake.amount == 100.0
|
||||
|
||||
async def test_create_stake_maximum_amount(self, staking_service, agent_metrics):
|
||||
"""Test creating stake with large amount"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create stake with large amount
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=100000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
assert stake is not None
|
||||
assert stake.amount == 100000.0
|
||||
|
||||
async def test_auto_compound_enabled(self, staking_service, agent_metrics):
|
||||
"""Test creating stake with auto-compound enabled"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=True
|
||||
)
|
||||
|
||||
assert stake.auto_compound is True
|
||||
|
||||
async def test_multiple_stakes_same_agent(self, staking_service, agent_metrics):
|
||||
"""Test creating multiple stakes on same agent"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create first stake
|
||||
stake1 = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Create second stake
|
||||
stake2 = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=2000.0,
|
||||
lock_period=90,
|
||||
auto_compound=True
|
||||
)
|
||||
|
||||
# Verify both stakes created
|
||||
assert stake1.stake_id != stake2.stake_id
|
||||
assert stake1.amount == 1000.0
|
||||
assert stake2.amount == 2000.0
|
||||
|
||||
# Verify agent metrics updated with total
|
||||
updated_metrics = await staking_service.get_agent_metrics(agent_metrics.agent_wallet)
|
||||
assert updated_metrics.total_staked == 3000.0
|
||||
assert updated_metrics.staker_count == 1 # Same staker
|
||||
|
||||
async def test_get_user_stakes(self, staking_service, agent_metrics):
|
||||
"""Test retrieving all stakes for a user"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create multiple stakes
|
||||
await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=2000.0,
|
||||
lock_period=90,
|
||||
auto_compound=True
|
||||
)
|
||||
|
||||
# Get user stakes
|
||||
stakes = await staking_service.get_user_stakes(staker_address)
|
||||
|
||||
assert len(stakes) == 2
|
||||
assert all(stake.staker_address == staker_address for stake in stakes)
|
||||
|
||||
async def test_claim_rewards(self, staking_service, agent_metrics):
|
||||
"""Test claiming rewards for a stake"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Create a stake
|
||||
stake = await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=1000.0,
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
|
||||
# Add some accumulated rewards
|
||||
stake.accumulated_rewards = 50.0
|
||||
staking_service.session.commit()
|
||||
|
||||
# Claim rewards
|
||||
result = await staking_service.claim_rewards([stake.stake_id])
|
||||
|
||||
assert result is not None
|
||||
assert result["total_rewards"] == 50.0
|
||||
assert result["claimed_stakes"] == 1
|
||||
|
||||
async def test_update_agent_performance(self, staking_service, agent_metrics):
|
||||
"""Test updating agent performance metrics"""
|
||||
# Update performance
|
||||
updated_metrics = await staking_service.update_agent_performance(
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
accuracy=98.0,
|
||||
successful=True
|
||||
)
|
||||
|
||||
assert updated_metrics is not None
|
||||
# Service recalculates average based on all submissions
|
||||
assert updated_metrics.successful_submissions == 10 # 9 + 1
|
||||
assert updated_metrics.total_submissions == 11
|
||||
# Average is recalculated: (9*95 + 98) / 10 = 95.3
|
||||
assert updated_metrics.average_accuracy > 95.0
|
||||
|
||||
async def test_database_rollback_on_error(self, staking_service, agent_metrics):
|
||||
"""Test database rollback when stake creation fails"""
|
||||
staker_address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
||||
|
||||
# Get initial total staked
|
||||
initial_metrics = await staking_service.get_agent_metrics(agent_metrics.agent_wallet)
|
||||
initial_staked = initial_metrics.total_staked
|
||||
|
||||
# Try to create stake with invalid amount (should fail and rollback)
|
||||
try:
|
||||
await staking_service.create_stake(
|
||||
staker_address=staker_address,
|
||||
agent_wallet=agent_metrics.agent_wallet,
|
||||
amount=50.0, # Invalid (below minimum)
|
||||
lock_period=30,
|
||||
auto_compound=False
|
||||
)
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Verify database state unchanged (rollback worked)
|
||||
final_metrics = await staking_service.get_agent_metrics(agent_metrics.agent_wallet)
|
||||
assert final_metrics.total_staked == initial_staked
|
||||
257
tests/staking/README.md
Normal file
257
tests/staking/README.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# AITBC Staking Tests Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains tests for the AITBC staking system, including the AgentStaking smart contract and the Python staking service.
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── fixtures/
|
||||
│ └── staking_fixtures.py # Shared fixtures for staking tests
|
||||
├── services/
|
||||
│ └── test_staking_service.py # Service-level tests
|
||||
├── integration/
|
||||
│ └── test_staking_lifecycle.py # Integration tests
|
||||
└── staking/
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Test Files
|
||||
|
||||
### Service Tests (`tests/services/test_staking_service.py`)
|
||||
|
||||
Tests the Python staking service business logic.
|
||||
|
||||
**Status**: 8/8 tests passing ✓
|
||||
|
||||
**Tests Covered**:
|
||||
- Create stake with valid parameters
|
||||
- Get stake by ID
|
||||
- Get user stakes with filters
|
||||
- Add to stake
|
||||
- Calculate rewards
|
||||
- Unsupported agent validation
|
||||
- Invalid amount validation
|
||||
- APY calculation verification
|
||||
|
||||
**Dependencies**:
|
||||
- pytest
|
||||
- sqlmodel
|
||||
- SQLite (in-memory database)
|
||||
|
||||
**Run Command**:
|
||||
```bash
|
||||
/opt/aitbc/venv/bin/python -m pytest tests/services/test_staking_service.py -v
|
||||
```
|
||||
|
||||
### Integration Tests (`tests/integration/test_staking_lifecycle.py`)
|
||||
|
||||
Tests the complete staking lifecycle end-to-end.
|
||||
|
||||
**Status**: 4/4 tests passing ✓
|
||||
|
||||
**Tests Covered**:
|
||||
- Complete staking lifecycle (create, unbond, complete)
|
||||
- Stake accumulation over time
|
||||
- Multiple stakes on same agent
|
||||
- Stakes with different performance tiers
|
||||
|
||||
**Dependencies**:
|
||||
- pytest
|
||||
- sqlmodel
|
||||
- SQLite (in-memory database)
|
||||
|
||||
**Run Command**:
|
||||
```bash
|
||||
/opt/aitbc/venv/bin/python -m pytest tests/integration/test_staking_lifecycle.py -v
|
||||
```
|
||||
|
||||
### Contract Tests (`contracts/test/AgentStaking.test.js`)
|
||||
|
||||
Tests the AgentStaking smart contract using Hardhat.
|
||||
|
||||
**Status**: Blocked by compilation errors in unrelated contracts
|
||||
|
||||
**Tests Implemented**:
|
||||
- Create stake with valid parameters
|
||||
- Initiate unbonding after lock period
|
||||
- Complete unbonding after unbonding period
|
||||
|
||||
**Issue**: Unrelated contracts have DocstringParsingError and TypeError
|
||||
|
||||
**Run Command** (when unblocked):
|
||||
```bash
|
||||
cd /opt/aitbc/contracts
|
||||
npx hardhat test test/AgentStaking.test.js
|
||||
```
|
||||
|
||||
## Shared Fixtures (`tests/fixtures/staking_fixtures.py`)
|
||||
|
||||
Reusable fixtures for staking tests to avoid duplication.
|
||||
|
||||
**Available Fixtures**:
|
||||
- `db_session` - In-memory SQLite database
|
||||
- `staking_service` - StakingService instance
|
||||
- `agent_wallet` - Default test agent wallet
|
||||
- `staker_address` - Default test staker address
|
||||
- `agent_metrics` - GOLD tier agent metrics
|
||||
- `agent_metrics_bronze` - BRONZE tier agent metrics
|
||||
- `agent_metrics_diamond` - DIAMOND tier agent metrics
|
||||
- `staking_pool` - Test staking pool
|
||||
- `stake_data` - Default stake creation data
|
||||
- `large_stake_data` - Large stake data
|
||||
- `small_stake_data` - Small stake data
|
||||
- `invalid_stake_data` - Invalid stake data
|
||||
- `created_stake` - Pre-created stake for testing
|
||||
- `active_stake` - Active stake in database
|
||||
- `unbonding_stake` - Unbonding stake in database
|
||||
- `completed_stake` - Completed stake in database
|
||||
- `multiple_stakes` - Multiple stakes for testing
|
||||
|
||||
**Helper Functions**:
|
||||
- `calculate_expected_apy()` - Calculate expected APY
|
||||
- `get_tier_multiplier()` - Get tier multiplier
|
||||
- `get_lock_multiplier()` - Get lock period multiplier
|
||||
|
||||
## Test Runner
|
||||
|
||||
A dedicated test runner script is available to execute all staking tests:
|
||||
|
||||
**Location**: `/opt/aitbc/scripts/testing/run_staking_tests.sh`
|
||||
|
||||
**Run Command**:
|
||||
```bash
|
||||
/opt/aitbc/scripts/testing/run_staking_tests.sh
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Runs service, integration, and contract tests
|
||||
- Generates combined test report
|
||||
- Saves logs to `/var/log/aitbc/tests/staking/`
|
||||
- Provides pass/fail status for each test suite
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Required Test Data
|
||||
- Agent wallet addresses (for different performance tiers)
|
||||
- Staker addresses (for creating stakes)
|
||||
- Agent performance metrics (accuracy, success rates, submission counts)
|
||||
- Staking pool data (initial state)
|
||||
|
||||
### Test Data Generation
|
||||
Test data can be generated using the fixtures or by creating test data manually in fixtures.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Deprecation Warnings
|
||||
- **Issue**: 63 deprecation warnings about `datetime.utcnow()`
|
||||
- **Impact**: Warnings only - tests pass successfully
|
||||
- **Fix**: Requires database migration to timezone-aware datetimes
|
||||
- **Status**: Deferred - not critical for functionality
|
||||
|
||||
### Contract Test Blocking
|
||||
- **Issue**: Compilation errors in unrelated contracts
|
||||
- **Error Types**: DocstringParsingError, TypeError
|
||||
- **Impact**: Cannot run AgentStaking contract tests
|
||||
- **Fix Options**:
|
||||
1. Fix compilation errors in affected contracts (proper solution)
|
||||
2. Isolate AgentStaking testing with separate Hardhat config
|
||||
3. Use mock contract deployment for initial testing
|
||||
- **Status**: Deferred - service and integration tests provide good coverage
|
||||
|
||||
## Test Execution
|
||||
|
||||
### Run All Staking Tests
|
||||
```bash
|
||||
/opt/aitbc/scripts/testing/run_staking_tests.sh
|
||||
```
|
||||
|
||||
### Run Service Tests Only
|
||||
```bash
|
||||
/opt/aitbc/venv/bin/python -m pytest tests/services/test_staking_service.py -v
|
||||
```
|
||||
|
||||
### Run Integration Tests Only
|
||||
```bash
|
||||
/opt/aitbc/venv/bin/python -m pytest tests/integration/test_staking_lifecycle.py -v
|
||||
```
|
||||
|
||||
### Run Contract Tests Only (when unblocked)
|
||||
```bash
|
||||
cd /opt/aitbc/contracts
|
||||
npx hardhat test test/AgentStaking.test.js
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Current Coverage
|
||||
- **Service Tests**: 8/8 tests passing (100%)
|
||||
- **Integration Tests**: 4/4 tests passing (100%)
|
||||
- **Contract Tests**: 0/3 tests (blocked)
|
||||
|
||||
### Coverage Areas
|
||||
- ✅ Stake creation and validation
|
||||
- ✅ APY calculation
|
||||
- ✅ Unbonding operations
|
||||
- ✅ Reward calculation
|
||||
- ✅ Agent metrics management
|
||||
- ✅ Staking pool operations
|
||||
- ❌ Contract deployment (blocked)
|
||||
- ❌ Contract execution (blocked)
|
||||
- ❌ Contract events (blocked)
|
||||
|
||||
## Future Improvements
|
||||
|
||||
### High Priority
|
||||
- [ ] Improve service test coverage with edge cases
|
||||
- [ ] Add error handling tests
|
||||
- [ ] Add performance tests
|
||||
|
||||
### Medium Priority
|
||||
- [ ] Create test data generator script
|
||||
- [ ] Add CI/CD integration
|
||||
- [ ] Implement test parallelization
|
||||
- [ ] Add performance benchmarks
|
||||
|
||||
### Low Priority (When Contract Tests Unblocked)
|
||||
- [ ] Fix contract compilation errors
|
||||
- [ ] Run contract tests
|
||||
- [ ] Add contract-service integration tests
|
||||
- [ ] Implement comprehensive end-to-end tests
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service Tests Fail
|
||||
1. Check SQLite in-memory database initialization
|
||||
2. Verify SQLModel imports are correct
|
||||
3. Check session commit and refresh operations
|
||||
4. Review fixture dependencies
|
||||
|
||||
### Integration Tests Fail
|
||||
1. Verify service test dependencies
|
||||
2. Check database session management
|
||||
3. Ensure fixtures are properly isolated
|
||||
4. Review time simulation logic
|
||||
|
||||
### Contract Tests Fail
|
||||
1. Check Hardhat configuration
|
||||
2. Verify contract dependencies (AIToken, PerformanceVerifier)
|
||||
3. Ensure contract compilation succeeds
|
||||
4. Review gas limits and transaction parameters
|
||||
|
||||
## Reports
|
||||
|
||||
Test reports are generated in `/var/log/aitbc/tests/staking/`:
|
||||
- `service_tests_*.log` - Service test output
|
||||
- `integration_tests_*.log` - Integration test output
|
||||
- `contract_tests_*.log` - Contract test output
|
||||
- `staking_test_report_*.txt` - Combined test report
|
||||
|
||||
## References
|
||||
|
||||
- **Test Plan**: `/opt/aitbc/tests/contracts/staking_test_plan.md`
|
||||
- **Implementation Plan**: `/root/.windsurf/plans/staking-high-priority-tests-6c2d50.md`
|
||||
- **Service Code**: `/opt/aitbc/apps/coordinator-api/src/app/services/staking_service.py`
|
||||
- **Domain Models**: `/opt/aitbc/apps/coordinator-api/src/app/domain/bounty.py`
|
||||
Reference in New Issue
Block a user