rmi-backend/tests/unit/test_launch_fairness.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

335 lines
12 KiB
Python

"""
Tests for Launch Fairness Analyzer.
"""
import unittest
from app.launch_fairness_analyzer import (
FairnessSignal,
FairnessSignalResult,
LaunchFairnessResult,
Severity,
_detect_bot_activity,
_detect_bundled_launch,
_detect_concentrated_holders,
_detect_lp_manipulation,
_detect_presale_concentration,
_detect_rapid_dump,
_detect_sniped_distribution,
_risk_level_from_score,
_severity_from_score,
analyze_launch_fairness,
)
class TestLaunchFairnessAnalyzer(unittest.TestCase):
"""Test the main analyze_launch_fairness function."""
def test_basic_analysis(self) -> None:
"""Should produce a valid fairness analysis from a token address."""
import asyncio
result = asyncio.run(
analyze_launch_fairness(
"0x1234567890abcdef1234567890abcdef12345678",
chain="ethereum",
simulate_data=True,
)
)
self.assertIn("token_address", result)
self.assertEqual(
result["token_address"],
"0x1234567890abcdef1234567890abcdef12345678",
)
self.assertIn("fairness_score", result)
self.assertIn("risk_level", result)
self.assertIn("signals", result)
self.assertIn("summary", result)
def test_solana_address(self) -> None:
"""Solana address format should be detected."""
import asyncio
sol_addr = "AbCdEf1234567890AbCdEf1234567890AbCdEf1234567890AbCdEf1234567890"
result = asyncio.run(analyze_launch_fairness(sol_addr, chain="auto", simulate_data=True))
self.assertIn("solana", result["chain"])
def test_invalid_address(self) -> None:
"""Invalid address should produce warnings but not crash."""
import asyncio
result = asyncio.run(analyze_launch_fairness("invalid!", chain="ethereum", simulate_data=True))
self.assertIn("warnings", result)
self.assertTrue(len(result["warnings"]) > 0)
def test_analysis_has_signals(self) -> None:
"""Analysis should return all signal types."""
import asyncio
result = asyncio.run(
analyze_launch_fairness(
"0xabcdef1234567890abcdef1234567890abcdef12",
chain="ethereum",
simulate_data=True,
)
)
signal_names = {s["signal"] for s in result["signals"]}
expected_signals = {
"sniped_distribution",
"bundled_launch",
"concentrated_top_holders",
"lp_manipulation",
"bot_activity",
"presale_concentration",
"rapid_dump_signal",
}
self.assertEqual(signal_names, expected_signals)
def test_analysis_time_is_measured(self) -> None:
"""Analysis time should be a positive number."""
import asyncio
result = asyncio.run(
analyze_launch_fairness(
"0xdead000000000000000000000000000000000000",
chain="ethereum",
simulate_data=True,
)
)
self.assertGreater(result["analysis_time_ms"], 0)
class TestSignalDetection(unittest.TestCase):
"""Test individual signal detectors."""
def test_sniped_detection_no_data(self) -> None:
"""No data should return undetected."""
result = _detect_sniped_distribution("0xabc", "ethereum")
self.assertFalse(result.detected)
self.assertIn("No transaction data", result.details)
def test_sniped_detection_with_data(self) -> None:
"""Multiple same-block buys should trigger sniper detection."""
txs = [
{
"from": f"0x{i:040x}",
"to": "0xtoken",
"block_number": 1,
"amount_usd": 1000,
"type": "buy",
}
for i in range(5)
]
result = _detect_sniped_distribution("0xabc", "ethereum", txs)
self.assertTrue(result.detected)
self.assertGreaterEqual(result.score, 0.4)
def test_bundle_detection_no_data(self) -> None:
"""No data should return undetected."""
result = _detect_bundled_launch("0xabc", "ethereum", [])
self.assertFalse(result.detected)
self.assertIn("Insufficient transaction data", result.details)
def test_bundle_detection_with_funders(self) -> None:
"""Multiple wallets funded by same source should trigger bundling."""
txs = [
{
"from": f"0x{i:040x}",
"to": "0xtoken",
"funded_by": "0xfunder123",
"amount_usd": 5000,
"block_number": 1,
}
for i in range(5)
]
result = _detect_bundled_launch("0xabc", "ethereum", txs)
self.assertTrue(result.detected)
self.assertGreaterEqual(result.score, 0.2)
def test_concentration_high(self) -> None:
"""90%+ concentration should be critical."""
holders = [{"address": f"0x{i:040x}", "balance": 100_000_000} for i in range(3)]
holders.append({"address": "0xsmall", "balance": 1_000_000})
result = _detect_concentrated_holders(holders)
# Top 3 hold 300M out of 301M = ~99.7%
self.assertTrue(result.detected)
self.assertEqual(result.severity, Severity.CRITICAL)
def test_concentration_low(self) -> None:
"""Low concentration should not be flagged."""
holders = [{"address": f"0x{i:040x}", "balance": 1_000_000} for i in range(100)]
result = _detect_concentrated_holders(holders)
# Top 10 hold 10M out of 100M = 10%
self.assertFalse(result.detected)
def test_concentration_no_data(self) -> None:
"""No holder data should return undetected."""
result = _detect_concentrated_holders([])
self.assertFalse(result.detected)
def test_lp_manipulation_delayed(self) -> None:
"""Delayed LP addition should be flagged."""
txs = [
{
"from": "0xbuyer",
"to": "0xtoken",
"block_number": 5,
"type": "buy",
"amount_usd": 100,
}
]
lp_data = {"add_delay_blocks": 500, "lp_token_concentration": 0.0}
result = _detect_lp_manipulation(lp_data, txs)
self.assertTrue(result.detected)
self.assertGreaterEqual(result.score, 0.4)
def test_lp_manipulation_removed(self) -> None:
"""LP removed should be flagged as high severity."""
txs = [
{
"from": "0xevil",
"to": "0xtoken",
"type": "remove_liquidity",
"block_number": 10,
"amount_usd": 50000,
}
]
result = _detect_lp_manipulation({}, txs)
self.assertTrue(result.detected)
self.assertGreaterEqual(result.score, 0.4)
def test_bot_detection_high_tx(self) -> None:
"""High transaction count from same wallet should be bot flagged."""
txs = [
{
"from": "0xbotwallet",
"to": "0xother",
"type": "swap",
"amount_usd": 100,
"timestamp": 1700000000 + i,
"gas_price_gwei": 50,
}
for i in range(10)
]
result = _detect_bot_activity(txs)
self.assertTrue(result.detected)
def test_bot_detection_no_data(self) -> None:
"""No data should return undetected."""
result = _detect_bot_activity([])
self.assertFalse(result.detected)
self.assertIn("Insufficient transaction data", result.details)
def test_presale_concentration_high(self) -> None:
"""50%+ presale should be critical."""
presale = {
"presale_allocation_pct": 60.0,
"participant_count": 100,
"insider_allocation_pct": 5.0,
"vc_allocation_pct": 10.0,
}
result = _detect_presale_concentration(presale)
self.assertTrue(result.detected)
self.assertEqual(result.severity, Severity.CRITICAL)
def test_presale_concentration_missing(self) -> None:
"""No presale data should return undetected."""
result = _detect_presale_concentration(None)
self.assertFalse(result.detected)
def test_rapid_dump_no_sells(self) -> None:
"""No early sells should return undetected."""
txs = [
{
"from": "0xbuyer",
"to": "0xtoken",
"type": "buy",
"block_number": 1,
"amount_usd": 1000,
}
]
result = _detect_rapid_dump(txs)
self.assertFalse(result.detected)
def test_rapid_dump_detected(self) -> None:
"""Large early sells should be flagged."""
txs = [
{
"from": f"0x{i:040x}",
"to": "0xtoken",
"type": "sell",
"block_number": 3,
"amount_usd": 50000,
}
for i in range(3)
]
result = _detect_rapid_dump(txs)
self.assertTrue(result.detected)
self.assertGreaterEqual(result.score, 0.6)
class TestScoring(unittest.TestCase):
"""Test scoring utilities."""
def test_severity_mapping(self) -> None:
self.assertEqual(_severity_from_score(0.9), Severity.CRITICAL)
self.assertEqual(_severity_from_score(0.7), Severity.HIGH)
self.assertEqual(_severity_from_score(0.5), Severity.MODERATE)
self.assertEqual(_severity_from_score(0.3), Severity.LOW)
self.assertEqual(_severity_from_score(0.1), Severity.NONE)
def test_risk_level_mapping(self) -> None:
self.assertEqual(_risk_level_from_score(90), "low")
self.assertEqual(_risk_level_from_score(70), "medium")
self.assertEqual(_risk_level_from_score(50), "high")
self.assertEqual(_risk_level_from_score(30), "critical")
class TestSerialization(unittest.TestCase):
"""Test serialization of result objects."""
def test_fairness_signal_result_to_dict(self) -> None:
signal = FairnessSignalResult(
signal=FairnessSignal.SNIPED_DISTRIBUTION,
detected=True,
severity=Severity.HIGH,
score=0.75,
details="Sniping detected",
evidence=["Block 1: 5 wallets bought"],
)
d = signal.to_dict()
self.assertEqual(d["signal"], "sniped_distribution")
self.assertTrue(d["detected"])
self.assertEqual(d["severity"], "high")
self.assertEqual(d["score"], 0.75)
def test_launch_fairness_result_to_dict(self) -> None:
result = LaunchFairnessResult(token_address="0xabc", chain="ethereum")
result.fairness_score = 45.0
result.risk_level = "high"
result.summary = "High risk - multiple signals"
result.signals = [
FairnessSignalResult(
signal=FairnessSignal.BUNDLED_LAUNCH,
detected=True,
score=0.6,
)
]
d = result.to_dict()
self.assertEqual(d["token_address"], "0xabc")
self.assertEqual(d["chain"], "ethereum")
self.assertEqual(d["fairness_score"], 45.0)
self.assertEqual(d["risk_level"], "high")
self.assertEqual(len(d["signals"]), 1)
def test_empty_result_serialization(self) -> None:
result = LaunchFairnessResult(token_address="0xempty", chain="ethereum")
d = result.to_dict()
self.assertEqual(d["token_address"], "0xempty")
self.assertEqual(d["fairness_score"], 100.0)
self.assertIn("summary", d)
self.assertIn("analysis_time_ms", d)
if __name__ == "__main__":
unittest.main()