File size: 16,006 Bytes
da15be0 99e0310 545ebcf da15be0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | #!/usr/bin/env python3
"""
scripts/test_feed_client.py — the Redis pool feed, against a real server.
venv/bin/python scripts/test_feed_client.py
Starts a throwaway redis-server on a spare port when one is available and
drives modules/feed_client.py through the real contract. When no
redis-server binary exists (a bare CI image), the server-backed section is
skipped and the failure-mode section still runs in full — those are the
checks that matter most and none of them need a live Redis.
THE SAFETY PROPERTY UNDER TEST, stated once:
A wrong "yes, quote it" costs one Jupiter call.
A wrong "no, skip it" costs a trade.
So every uncertainty — feed off, Redis down, key missing, JSON malformed,
value stale — must resolve to YES. The only "no" allowed is positive
evidence that nothing moved. Half these checks exist to prove the asymmetry
holds in the failing direction, because that is the direction that is
silent when it breaks.
"""
from __future__ import annotations
import importlib
import json
import os
import shutil
import socket
import subprocess
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
FAILURES: list[str] = []
CHECKS = 0
def check(label: str, condition: bool, detail: str = "") -> None:
global CHECKS
CHECKS += 1
if condition:
print(f" \033[32m✓\033[0m {label}")
else:
FAILURES.append(f"{label}{f' — {detail}' if detail else ''}")
print(f" \033[31m✗\033[0m {label}" + (f" — {detail}" if detail else ""))
def _free_port() -> int:
with socket.socket() as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def test_failure_modes() -> None:
"""Every unknown resolves to 'ask Jupiter'. No server needed."""
print("\n\033[1m1. every uncertainty defaults to asking Jupiter\033[0m")
import modules.feed_client as fc
importlib.reload(fc)
fc.ENABLED = False
ok, why = fc.FeedClient().should_quote("r")
check("FEED_ENABLED=false -> quote", ok, why)
importlib.reload(fc)
fc.ENABLED = True
fc.REDIS_URL = "redis://127.0.0.1:1/0" # nothing listening
ok, why = fc.FeedClient().should_quote("r")
check("Redis unreachable -> quote (and no crash)", ok, why)
client = fc.FeedClient()
client._client()
check("an outage is remembered, not retried per lookup",
client._unavailable_until > time.monotonic(),
"a down Redis on every route of every cycle would replace the "
"latency this module exists to remove")
def test_against_real_redis() -> None:
print("\n\033[1m2. the real contract, against a real server\033[0m")
if not shutil.which("redis-server"):
print(" \033[33m·\033[0m redis-server not installed — section skipped")
return
try:
import redis # noqa: F401
except ImportError:
print(" \033[33m·\033[0m redis package not installed — section skipped")
return
port = _free_port()
proc = subprocess.Popen(
["redis-server", "--port", str(port), "--save", "", "--appendonly", "no"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
import redis
url = f"redis://127.0.0.1:{port}/0"
r = None
for _ in range(50):
try:
r = redis.Redis.from_url(url, decode_responses=True)
r.ping()
break
except Exception: # noqa: BLE001
time.sleep(0.1)
if r is None:
print(" \033[33m·\033[0m server never came up — section skipped")
return
import modules.feed_client as fc
importlib.reload(fc)
fc.ENABLED = True
fc.REDIS_URL = url
fc.MAX_AGE_MS = 250.0
fc.STALE_ACTION = "skip"
feed = fc.FeedClient()
def write(dex: str, ra: int, rb: int, slot: int, age_ms: int = 0) -> None:
r.set(f"feed:sol-usdc:{dex}:state", json.dumps({
"reserve_a": ra, "reserve_b": rb,
"mint_a": "So1", "mint_b": "EPj",
"slot": slot, "ts_ms": int(time.time() * 1000) - age_ms}))
r.sadd("feed:sol-usdc:pools", dex)
r.flushall()
write("raydium", 100, 200, 1000)
check("pools are discovered, not hardcoded", feed.pools() == ["raydium"])
ok, _ = feed.should_quote("USDC->SOL->USDC")
check("first look quotes (nothing seen yet)", ok)
ok, why = feed.should_quote("USDC->SOL->USDC")
check("unchanged pool is SKIPPED — the entire saving", not ok, why)
write("raydium", 101, 199, 1001)
ok, _ = feed.should_quote("USDC->SOL->USDC")
check("a moved pool quotes again", ok)
# Reserves identical, slot advanced: must still count as moved. A
# rounding-sized swap can leave both reserves numerically equal.
write("raydium", 101, 199, 1002)
ok, _ = feed.should_quote("USDC->SOL->USDC")
check("slot change alone counts as movement", ok,
"reserves-only fingerprinting would miss a tiny swap")
# Per-route isolation: another route has its own memory.
ok, _ = feed.should_quote("USDT->SOL->USDT")
check("a different route is judged independently", ok)
write("raydium", 300, 400, 2000, age_ms=900)
ok, why = feed.should_quote("USDC->SOL->USDC")
check("stale feed SKIPS by default", not ok, why)
fc.STALE_ACTION = "jupiter"
feed2 = fc.FeedClient()
ok, why = feed2.should_quote("USDC->SOL->USDC")
check("stale feed falls back when told to", ok, why)
# A malformed row is a miss, never an exception.
r.flushall()
r.sadd("feed:sol-usdc:pools", "raydium")
r.set("feed:sol-usdc:raydium:state", "{{{ not json")
ok, why = fc.FeedClient().should_quote("USDC->SOL->USDC")
check("malformed JSON -> quote, no crash", ok, why)
# Listed pool with no state key at all.
r.delete("feed:sol-usdc:raydium:state")
ok, why = fc.FeedClient().should_quote("USDC->SOL->USDC")
check("missing state key -> quote, no crash", ok, why)
# ── THE SILENT MISMATCH ───────────────────────────────────────
# The two briefs specified `feed:sol-usdc:<dex>:state` and
# `feed:solusdc:<dex>`. Read the wrong one and this module works
# perfectly: it finds nothing, falls back to Jupiter, reports no
# error, and buys exactly nothing forever. describe() is the only
# thing standing between that and a week of "why is the budget
# still at 99%".
print("\n\033[1m2b. a key mismatch cannot hide\033[0m")
r.flushall()
r.set("feed:solusdc:raydium", json.dumps({ # the WRONG prefix
"reserve_a": 1, "reserve_b": 2, "slot": 1,
"ts_ms": int(time.time() * 1000)}))
r.sadd("feed:solusdc:pools", "raydium")
importlib.reload(fc)
fc.ENABLED = True
fc.REDIS_URL = url
fc.KEY_PREFIX = "feed:sol-usdc" # what we read
d = fc.FeedClient().describe()
check("reads nothing under its own prefix", d["pools"] == [])
check("but SEES the writer's actual prefix",
"feed:solusdc" in d["found_prefixes"], str(d["found_prefixes"]))
check("and calls it a mismatch, not a quiet market",
any("MISMATCH" in p for p in d["problems"]), str(d["problems"]))
check("and prints the exact command that fixes it",
"FEED_KEY_PREFIX=feed:solusdc" in d["hint"], d["hint"])
# Reading the right prefix must produce NO problem — a diagnostic
# that cries wolf on a healthy feed gets ignored on a broken one.
fc.KEY_PREFIX = "feed:solusdc"
d = fc.FeedClient().describe()
check("a correctly-configured feed reports clean",
d["pools"] == ["raydium"] and not d["problems"], str(d))
# Redis up, writer never started: a different problem, said
# differently. Blaming the prefix here would send the operator to
# change a setting that is already right.
r.flushall()
d = fc.FeedClient().describe()
check("no data at all is blamed on the WRITER, not the prefix",
any("not running" in w for w in d["warnings"]) and not d["hint"],
str(d["warnings"]))
# ── BLOCKER vs NOISE ──────────────────────────────────────────
# /doctor's ⛔ list is for things that stop the bot trading, and
# it is read top-down. On the live box this module put
#
# • pool feed: Redis unreachable ... trading is unaffected ...
#
# in that list, directly above the IAM denial that genuinely was
# blocking every trade. A false blocker is worse than a warning:
# it dilutes the one list the operator is meant to act on, and
# this one said "trading is unaffected" in its own text.
check("an absent writer is NOT a blocker",
not d["problems"],
"expected until garden-feed exists; the Jupiter fallback is "
"where the bot has always been")
importlib.reload(fc)
fc.ENABLED = True
fc.REDIS_URL = "redis://127.0.0.1:1/0" # nothing listening
d = fc.FeedClient().describe()
check("an unreachable Redis is NOT a blocker either",
not d["problems"] and d["warnings"], str(d))
# But a MISMATCH still is — that one is silently costing something.
importlib.reload(fc)
fc.ENABLED = True
fc.REDIS_URL = url
fc.KEY_PREFIX = "feed:sol-usdc"
r.set("feed:solusdc:raydium", json.dumps({
"reserve_a": 1, "reserve_b": 2, "slot": 1,
"ts_ms": int(time.time() * 1000)}))
r.sadd("feed:solusdc:pools", "raydium")
d = fc.FeedClient().describe()
check("a real KEY MISMATCH is still a blocker",
any("MISMATCH" in p for p in d["problems"]) and d["hint"],
"this is the one case that is silently costing budget")
# MISMATCH is the ONLY blocker. A writer that has started and not
# yet registered a pool is a writer that is STARTING — waiting is
# the correct response, so it must not enter the ⛔ list. Qodo
# caught this contradicting the contract stated in describe()'s own
# docstring, two screens above the code that broke it.
r.flushall()
r.set("feed:sol-usdc:raydium:state", json.dumps({
"reserve_a": 1, "reserve_b": 2, "slot": 1,
"ts_ms": int(time.time() * 1000)})) # no :pools member
d = fc.FeedClient().describe()
check("a writer with no pools registered yet is NOT a blocker",
not d["problems"] and d["warnings"], str(d))
# Every warning must name its own subsystem. /doctor prefixes
# problems with "pool feed:" but forwards warnings verbatim, so a
# bare "could not scan Redis" loses its owner among the warnings of
# five other subsystems.
for w in d["warnings"]:
check("warning names its subsystem", w.startswith("pool feed"),
w[:80])
# And the chat path must give the same reason /doctor does.
from modules.bot_chat import BotChat
importlib.reload(fc)
fc.ENABLED = True
fc.REDIS_URL = "redis://127.0.0.1:1/0"
said = BotChat()._say_feed()
check("chat reports the WARNING, not a bare 'no data'",
"no data" not in said and "unreachable" in said,
"reading only problems left the real reason — and the command "
"that silences it — unread in warnings")
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
def test_never_prices_locally() -> None:
"""The design claim: this module does no AMM maths, on purpose."""
print("\n\033[1m3. it computes no price — that is the point\033[0m")
src = (Path(__file__).resolve().parent.parent
/ "modules" / "feed_client.py").read_text(encoding="utf-8")
code = "\n".join(l for l in src.splitlines()
if not l.lstrip().startswith("#"))
# Reserves alone cannot price a concentrated-liquidity pool, and this
# deployment's real routes are Whirlpool / Meteora DLMM / Raydium CLMM.
# A constant-product formula applied to those would be confidently wrong
# on live money, so it must not appear here at all.
for forbidden in ("* reserve_b", "reserve_a *", "/ reserve_a", "/ reserve_b",
"amount_out", "get_amount_out", "0.9975", "9975"):
check(f"no AMM arithmetic: `{forbidden}`", forbidden not in code,
"reserves cannot price a CLMM pool — see the module docstring")
from modules.feed_client import FeedClient
check("the public question is should_quote, not price",
hasattr(FeedClient, "should_quote") and not hasattr(FeedClient, "price"))
def test_wired_into_the_scan_loop() -> None:
"""Built but not called is the same as not built."""
print("\n\033[1m4. the gate is actually wired into the scan loop\033[0m")
src = (Path(__file__).resolve().parent.parent
/ "modules" / "solana_arb.py").read_text(encoding="utf-8")
code = "\n".join(l for l in src.splitlines()
if not l.lstrip().startswith("#"))
check("scan loop imports the feed", "from modules.feed_client import get_feed" in code)
check("and calls should_quote", "should_quote(route_key)" in code)
# ORDERING: the gate must sit before the quote it exists to avoid, and
# after the prune plan — those answer different questions and swapping
# them would spend budget on routes the planner had already dropped.
gate = code.find("should_quote(route_key)")
quote = code.find("await self.scan_route(client,")
plan = code.find("cycle_plan.get(")
check("gate runs BEFORE the quote", 0 < gate < quote,
"a gate after the call it saves is decoration")
check("gate runs AFTER the prune plan", 0 < plan < gate)
# A failure inside the gate must not stop the scan.
check("the gate cannot break a cycle",
"feed check skipped" in src and "feed_ok, feed_why = True" in code,
"any error must fall through to quoting, not raise")
def test_inert_by_default() -> None:
"""FEED_ENABLED=false must be byte-identical to today's behaviour."""
print("\n\033[1m5. off by default, provably\033[0m")
import modules.feed_client as fc
saved = os.environ.pop("FEED_ENABLED", None)
try:
importlib.reload(fc)
check("FEED_ENABLED defaults to false", fc.ENABLED is False)
ok, why = fc.FeedClient().should_quote("USDC->SOL")
check("so the gate always says quote", ok and "disabled" in why,
"an upgrade must change nothing until it is asked to")
finally:
if saved is not None:
os.environ["FEED_ENABLED"] = saved
importlib.reload(fc)
def main() -> int:
print("\n\033[1m══════ feed_client: trigger, not price ══════\033[0m")
test_failure_modes()
test_against_real_redis()
test_never_prices_locally()
test_wired_into_the_scan_loop()
test_inert_by_default()
print()
if FAILURES:
print(f"\033[31m❌ {len(FAILURES)} of {CHECKS} checks FAILED\033[0m")
for f in FAILURES:
print(f" • {f}")
print()
return 1
print(f"\033[32m✅ all {CHECKS} checks passed\033[0m\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|