| """ |
| Tests for Smart Money Flow Tracker. |
| """ |
|
|
| import os |
| import sys |
| import tempfile |
| from datetime import UTC, datetime, timedelta |
| from unittest.mock import AsyncMock, patch |
|
|
| import pytest |
|
|
| |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
|
|
| from app.smart_money_tracker import ( |
| DetectedMove, |
| FlowReport, |
| ImpactLevel, |
| MoveDirection, |
| SmartMoneyTracker, |
| SmartWallet, |
| WalletCategory, |
| run_smart_money_scan, |
| ) |
|
|
| |
| |
| |
|
|
|
|
| @pytest.fixture |
| def temp_data_dir(): |
| """Create a temporary data directory for tests.""" |
| with tempfile.TemporaryDirectory() as tmpdir: |
| yield tmpdir |
|
|
|
|
| @pytest.fixture |
| def sample_wallets(): |
| """Create sample smart wallets for testing.""" |
| return [ |
| SmartWallet( |
| address="0x1234567890abcdef1234567890abcdef12345678", |
| chain="ethereum", |
| label="Test Whale", |
| category=WalletCategory.WHALE, |
| reputation_score=85.0, |
| confidence=0.8, |
| tags=["whale", "test"], |
| total_value_usd=5_000_000, |
| ), |
| SmartWallet( |
| address="A3eME5C1Z7YpEkna5K5WJs8xJ9KXGxFwVvxn3BbmxM7G", |
| chain="solana", |
| label="Solana Whale", |
| category=WalletCategory.WHALE, |
| reputation_score=70.0, |
| confidence=0.6, |
| tags=["solana"], |
| total_value_usd=2_500_000, |
| ), |
| SmartWallet( |
| address="0xabcdef1234567890abcdef1234567890abcdef12", |
| chain="ethereum", |
| label="Market Maker", |
| category=WalletCategory.MARKET_MAKER, |
| reputation_score=90.0, |
| confidence=0.9, |
| tags=["mm", "high-freq"], |
| total_value_usd=50_000_000, |
| ), |
| ] |
|
|
|
|
| @pytest.fixture |
| def tracker(temp_data_dir, sample_wallets): |
| """Create tracker with sample wallets.""" |
| t = SmartMoneyTracker(data_dir=temp_data_dir) |
| t._wallets = sample_wallets |
| t._save_wallets() |
| return t |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TestSmartWallet: |
| def test_summary_formats_correctly(self): |
| """Test that summary() produces expected output format.""" |
| wallet = SmartWallet( |
| address="0x1234567890abcdef1234567890abcdef12345678", |
| chain="ethereum", |
| label="Test Wallet", |
| category=WalletCategory.WHALE, |
| total_value_usd=1_000_000, |
| reputation_score=75.0, |
| ) |
| summary = wallet.summary() |
| assert "Test Wallet" in summary |
| assert "ethereum" in summary |
| assert "1,000,000" in summary |
| assert "75/100" in summary |
| assert "π" in summary |
|
|
| def test_vc_fund_category_icon(self): |
| """Test that VC fund has correct icon.""" |
| wallet = SmartWallet( |
| address="0xabc", |
| chain="ethereum", |
| label="Paradigm", |
| category=WalletCategory.VC_FUND, |
| ) |
| assert "π¦" in wallet.summary() |
|
|
| def test_category_scores(self): |
| """Test that reputation scores transfer correctly.""" |
| wallet = SmartWallet( |
| address="0xabc", |
| chain="ethereum", |
| label="Test", |
| category=WalletCategory.TOP_TRADER, |
| reputation_score=88.0, |
| ) |
| assert wallet.reputation_score == 88.0 |
|
|
| def test_default_values(self): |
| """Test default values for optional fields.""" |
| wallet = SmartWallet( |
| address="0xabc", |
| chain="ethereum", |
| label="Test", |
| category=WalletCategory.UNKNOWN, |
| ) |
| assert wallet.tags == [] |
| assert wallet.total_value_usd == 0.0 |
| assert wallet.reputation_score == 50.0 |
| assert wallet.confidence == 0.5 |
| assert wallet.tx_count == 0 |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TestDetectedMove: |
| @pytest.fixture |
| def sample_wallet(self): |
| return SmartWallet( |
| address="0x1234", |
| chain="ethereum", |
| label="Test Whale", |
| category=WalletCategory.WHALE, |
| ) |
|
|
| def test_enter_move_summary(self, sample_wallet): |
| """Test that enter moves show green icon.""" |
| move = DetectedMove( |
| wallet=sample_wallet, |
| token_address="0xtoken", |
| token_symbol="RUG", |
| chain="ethereum", |
| direction=MoveDirection.ENTER, |
| tx_hash="0xabc123", |
| timestamp=datetime.now(UTC), |
| amount=1000, |
| value_usd=50_000, |
| impact=ImpactLevel.HIGH, |
| context="Opened new position", |
| ) |
| summary = move.summary() |
| assert "π’" in summary |
| assert "ENTER" in summary |
| assert "RUG" in summary |
| |
| assert "50,000" in summary |
| assert "β οΈ" in summary |
|
|
| def test_exit_move_summary(self, sample_wallet): |
| """Test that exit moves show red icon.""" |
| move = DetectedMove( |
| wallet=sample_wallet, |
| token_address="0xtoken", |
| token_symbol="BTC", |
| chain="ethereum", |
| direction=MoveDirection.EXIT, |
| tx_hash="0xdef456", |
| timestamp=datetime.now(UTC), |
| amount=5, |
| value_usd=500_000, |
| impact=ImpactLevel.CRITICAL, |
| context="Full exit from BTC position", |
| ) |
| summary = move.summary() |
| assert "π΄" in summary |
| assert "EXIT" in summary |
| assert "π¨" in summary |
|
|
| def test_medium_impact_shows_correctly(self, sample_wallet): |
| """Test medium impact formatting.""" |
| move = DetectedMove( |
| wallet=sample_wallet, |
| token_address="", |
| token_symbol="ETH", |
| chain="ethereum", |
| direction=MoveDirection.ADD, |
| tx_hash="0xghi789", |
| timestamp=datetime.now(UTC), |
| amount=10, |
| value_usd=30_000, |
| impact=ImpactLevel.MEDIUM, |
| context="Added to position", |
| ) |
| summary = move.summary() |
| assert "π" in summary |
| assert "ADD" in summary in summary |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TestFlowReport: |
| @pytest.fixture |
| def sample_wallet(self): |
| return SmartWallet( |
| address="0x1234", |
| chain="ethereum", |
| label="Whale", |
| category=WalletCategory.WHALE, |
| ) |
|
|
| def make_move(self, wallet, value, impact, direction=MoveDirection.ENTER): |
| return DetectedMove( |
| wallet=wallet, |
| token_address="0xtoken", |
| token_symbol="TEST", |
| chain="ethereum", |
| direction=direction, |
| tx_hash=f"0x{hash(str(value))}", |
| timestamp=datetime.now(UTC), |
| amount=100, |
| value_usd=value, |
| impact=impact, |
| context=f"Test move ${value}", |
| ) |
|
|
| def test_top_moves_sorts_by_impact_then_value(self, sample_wallet): |
| """Test that top_moves returns highest impact first, sorted by value.""" |
| report = FlowReport() |
| report.moves = [ |
| self.make_move(sample_wallet, 200_000, ImpactLevel.MEDIUM), |
| self.make_move(sample_wallet, 1_000_000, ImpactLevel.CRITICAL), |
| self.make_move(sample_wallet, 50_000, ImpactLevel.LOW), |
| self.make_move(sample_wallet, 750_000, ImpactLevel.HIGH), |
| ] |
|
|
| top = report.top_moves(limit=3) |
| assert len(top) == 3 |
| assert top[0].impact == ImpactLevel.CRITICAL |
| assert top[1].impact == ImpactLevel.HIGH |
| assert top[2].impact == ImpactLevel.MEDIUM |
|
|
| def test_by_impact_filters_correctly(self, sample_wallet): |
| """Test filtering by impact level.""" |
| report = FlowReport() |
| report.moves = [ |
| self.make_move(sample_wallet, 1_000_000, ImpactLevel.CRITICAL), |
| self.make_move(sample_wallet, 500_000, ImpactLevel.HIGH), |
| self.make_move(sample_wallet, 100_000, ImpactLevel.MEDIUM), |
| ] |
|
|
| critical_moves = report.by_impact(ImpactLevel.CRITICAL) |
| assert len(critical_moves) == 1 |
| assert critical_moves[0].value_usd == 1_000_000 |
|
|
| def test_summary_includes_stats(self, sample_wallet): |
| """Test that summary contains key statistics.""" |
| report = FlowReport() |
| report.moves = [ |
| self.make_move(sample_wallet, 1_000_000, ImpactLevel.CRITICAL), |
| self.make_move(sample_wallet, 500_000, ImpactLevel.HIGH), |
| self.make_move(sample_wallet, 100_000, ImpactLevel.MEDIUM), |
| ] |
| report.wallets_scanned = 10 |
| report.chains_covered = ["ethereum", "solana"] |
| report.scan_duration_ms = 1500 |
|
|
| summary = report.summary() |
| assert "Smart Money Flow Report" in summary |
| assert "10 wallets" in summary |
| assert "ethereum" in summary |
| assert "solana" in summary |
| assert "$1,600,000" in summary or "$1,600,000" in summary |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TestSmartMoneyTracker: |
| def test_init_creates_data_dir(self, temp_data_dir): |
| """Test that tracker creates its data directory.""" |
| SmartMoneyTracker(data_dir=temp_data_dir) |
| assert os.path.exists(temp_data_dir) |
|
|
| def test_add_wallet(self, tracker, temp_data_dir): |
| """Test adding a new wallet to the registry.""" |
| new_wallet = SmartWallet( |
| address="0xnew", |
| chain="ethereum", |
| label="New Whale", |
| category=WalletCategory.WHALE, |
| ) |
| count_before = tracker.get_wallet_count() |
| tracker.add_wallet(new_wallet) |
| assert tracker.get_wallet_count() == count_before + 1 |
|
|
| |
| tracker2 = SmartMoneyTracker(data_dir=temp_data_dir) |
| assert tracker2.get_wallet_count() == count_before + 1 |
|
|
| def test_add_wallet_updates_existing(self, tracker): |
| """Test that adding an existing wallet updates it.""" |
| updated = SmartWallet( |
| address="0x1234567890abcdef1234567890abcdef12345678", |
| chain="ethereum", |
| label="Test Whale UPDATED", |
| category=WalletCategory.WHALE, |
| reputation_score=95.0, |
| ) |
| tracker.add_wallet(updated) |
| wallet = tracker.get_wallets(chain="ethereum")[0] |
| |
| assert wallet.label == "Test Whale UPDATED" |
| assert wallet.reputation_score == 95.0 |
|
|
| def test_remove_wallet(self, tracker): |
| """Test wallet removal.""" |
| count_before = tracker.get_wallet_count() |
| result = tracker.remove_wallet("0x1234567890abcdef1234567890abcdef12345678", "ethereum") |
| assert result is True |
| assert tracker.get_wallet_count() == count_before - 1 |
|
|
| def test_remove_nonexistent_wallet(self, tracker): |
| """Test removing a wallet that doesn't exist.""" |
| result = tracker.remove_wallet("0xnonexistent", "ethereum") |
| assert result is False |
|
|
| def test_get_wallets_filter_by_category(self, tracker): |
| """Test filtering wallets by category.""" |
| whales = tracker.get_wallets(category=WalletCategory.WHALE) |
| assert len(whales) == 2 |
|
|
| mm = tracker.get_wallets(category=WalletCategory.MARKET_MAKER) |
| assert len(mm) == 1 |
|
|
| def test_get_wallets_filter_by_chain(self, tracker): |
| """Test filtering wallets by chain.""" |
| eth_wallets = tracker.get_wallets(chain="ethereum") |
| assert len(eth_wallets) == 2 |
|
|
| sol_wallets = tracker.get_wallets(chain="solana") |
| assert len(sol_wallets) == 1 |
|
|
| def test_get_wallets_filter_none(self, tracker): |
| """Test getting all wallets with no filters.""" |
| wallets = tracker.get_wallets() |
| assert len(wallets) == 3 |
|
|
| def test_default_wallets_loaded(self, temp_data_dir): |
| """Test that default wallets are loaded when no saved data.""" |
| tracker = SmartMoneyTracker(data_dir=temp_data_dir) |
| |
| assert tracker.get_wallet_count() == 10 |
|
|
| def test_score_impact_critical(self, tracker): |
| """Test critical impact scoring.""" |
| wallet = SmartWallet( |
| address="0xcrit", |
| chain="ethereum", |
| label="Critical", |
| category=WalletCategory.WHALE, |
| reputation_score=85.0, |
| ) |
| impact = tracker._score_impact(wallet, 2_000_000, MoveDirection.ENTER) |
| assert impact == ImpactLevel.CRITICAL |
|
|
| def test_score_impact_high(self, tracker): |
| """Test high impact scoring.""" |
| wallet = SmartWallet( |
| address="0xhigh", |
| chain="ethereum", |
| label="High", |
| category=WalletCategory.WHALE, |
| reputation_score=70.0, |
| ) |
| impact = tracker._score_impact(wallet, 600_000, MoveDirection.REDUCE) |
| assert impact == ImpactLevel.HIGH |
|
|
| def test_score_impact_medium(self, tracker): |
| """Test medium impact scoring.""" |
| wallet = SmartWallet( |
| address="0xmed", |
| chain="ethereum", |
| label="Medium", |
| category=WalletCategory.WHALE, |
| reputation_score=60.0, |
| ) |
| impact = tracker._score_impact(wallet, 150_000, MoveDirection.ADD) |
| assert impact == ImpactLevel.MEDIUM |
|
|
| def test_score_impact_low(self, tracker): |
| """Test low impact scoring.""" |
| wallet = SmartWallet( |
| address="0xlow", |
| chain="ethereum", |
| label="Low", |
| category=WalletCategory.TOP_TRADER, |
| reputation_score=55.0, |
| ) |
| impact = tracker._score_impact(wallet, 20_000, MoveDirection.TRANSFER) |
| assert impact == ImpactLevel.LOW |
|
|
| def test_generate_context_enter(self, tracker): |
| """Test context generation for enter moves.""" |
| wallet = SmartWallet( |
| address="0xctx", |
| chain="ethereum", |
| label="Alpha Whale", |
| category=WalletCategory.WHALE, |
| ) |
| ctx = tracker._generate_context(MoveDirection.ENTER, wallet, 100_000) |
| assert "Alpha Whale" in ctx |
| assert "opened a new position" in ctx |
| assert "$100,000" in ctx |
|
|
| def test_generate_context_exit(self, tracker): |
| """Test context generation for exit moves.""" |
| wallet = SmartWallet( |
| address="0xctx", |
| chain="ethereum", |
| label="Trader", |
| category=WalletCategory.TOP_TRADER, |
| ) |
| ctx = tracker._generate_context(MoveDirection.EXIT, wallet, 500_000) |
| assert "fully exited" in ctx |
|
|
| def test_generate_context_add(self, tracker): |
| """Test context generation for add moves.""" |
| wallet = SmartWallet( |
| address="0xctx", chain="ethereum", label="Whale", category=WalletCategory.WHALE |
| ) |
| ctx = tracker._generate_context(MoveDirection.ADD, wallet, 50_000) |
| assert "added" in ctx |
|
|
| def test_generate_context_reduce(self, tracker): |
| """Test context generation for reduce moves.""" |
| wallet = SmartWallet( |
| address="0xctx", chain="ethereum", label="Whale", category=WalletCategory.WHALE |
| ) |
| ctx = tracker._generate_context(MoveDirection.REDUCE, wallet, 50_000) |
| assert "reduced" in ctx |
|
|
| @pytest.mark.asyncio |
| async def test_scan_empty_tracker(self, temp_data_dir): |
| """Test scanning with no wallets.""" |
| tracker = SmartMoneyTracker(data_dir=temp_data_dir) |
| |
| tracker._wallets = [] |
| report = await tracker.scan(lookback_minutes=60, max_wallets=10) |
| assert len(report.moves) == 0 |
| assert report.wallets_scanned == 0 |
| assert report.scan_duration_ms > 0 |
|
|
| @pytest.mark.asyncio |
| async def test_scan_with_mock_txs(self, tracker): |
| """Test scan with mocked transaction data.""" |
| |
| sample_tx = { |
| "tx_hash": "0xabc123def456", |
| "timestamp": datetime.now(UTC) - timedelta(minutes=30), |
| "chain": "ethereum", |
| "type": "transfer", |
| "from": "0x1234567890abcdef1234567890abcdef12345678", |
| "to": "0x9876543210fedcba9876543210fedcba98765432", |
| "value_usd": 100_000, |
| "token_address": "0xtoken123", |
| "token_symbol": "RUG", |
| "raw": {}, |
| } |
|
|
| with ( |
| patch.object(tracker, "_fetch_recent_tx", new=AsyncMock(return_value=[sample_tx])), |
| patch.object(tracker, "_get_token_safety", new=AsyncMock(return_value=45.0)), |
| ): |
| report = await tracker.scan(lookback_minutes=120, max_wallets=3) |
|
|
| |
| assert len(report.moves) > 0 |
| assert report.wallets_scanned == 3 |
| assert "ethereum" in report.chains_covered |
|
|
| def test_get_report_path(self, tracker, temp_data_dir): |
| """Test report path returns correct directory.""" |
| assert tracker.get_report_path() == temp_data_dir |
|
|
| def test_wallet_count(self, tracker): |
| """Test get_wallet_count returns correct count.""" |
| assert tracker.get_wallet_count() == 3 |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_run_smart_money_scan_callable(): |
| """Test that the module-level convenience function is callable.""" |
| |
| assert callable(run_smart_money_scan) |
|
|
|
|
| if __name__ == "__main__": |
| pytest.main([__file__, "-v"]) |
|
|