Skip to content

Commit de48381

Browse files
smypmsaclaude
andauthored
test: add auto-routing test coverage for PumpSwap graduated tokens (#17)
- 11 core-layer unit tests verifying graduated→pumpswap routing sequence (mocked RPC) - 10 command-layer unit tests verifying parameter forwarding (slippage, priority-fee, dry-run, confirm) through the fallback path and edge cases (not_found does not trigger fallback, pool-not-found after graduation surfaces correctly) - 3 surfpool integration tests verifying full auto-routing end-to-end against a forked mainnet node No production code changed. Test delta: +21 unit tests (339→360), +3 surfpool tests. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e665c38 commit de48381

3 files changed

Lines changed: 840 additions & 0 deletions

File tree

tests/test_commands/test_trade_cmd.py

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,3 +756,313 @@ def test_buy_json_output_has_expected_keys(tmp_path, monkeypatch):
756756
"explorer",
757757
}
758758
assert expected_keys.issubset(data.keys())
759+
760+
761+
# --- auto-routing command-layer tests ---
762+
763+
764+
def _setup_wallet(tmp_path, monkeypatch):
765+
"""Create a wallet and set env vars for command-layer tests."""
766+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
767+
monkeypatch.setenv("PUMPFUN_PASSWORD", "testpass")
768+
769+
from solders.keypair import Keypair
770+
771+
from pumpfun_cli.crypto import encrypt_keypair
772+
773+
config_dir = tmp_path / "pumpfun-cli"
774+
config_dir.mkdir()
775+
encrypt_keypair(Keypair(), "testpass", config_dir / "wallet.enc")
776+
777+
778+
def test_buy_graduated_fallback_forwards_slippage(tmp_path, monkeypatch):
779+
"""--slippage 5 forwarded to buy_pumpswap on graduated fallback."""
780+
_setup_wallet(tmp_path, monkeypatch)
781+
782+
with (
783+
patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy,
784+
patch("pumpfun_cli.commands.trade.buy_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
785+
):
786+
mock_buy.return_value = {"error": "graduated", "message": "Token has graduated."}
787+
mock_pumpswap.return_value = {
788+
"action": "buy",
789+
"venue": "pumpswap",
790+
"mint": _FAKE_MINT,
791+
"sol_spent": 0.01,
792+
"tokens_received": 100.0,
793+
"signature": "sig",
794+
"explorer": "https://solscan.io/tx/sig",
795+
}
796+
797+
result = runner.invoke(
798+
app,
799+
["--json", "--rpc", "http://rpc", "buy", "--slippage", "5", _FAKE_MINT, "0.01"],
800+
)
801+
802+
assert result.exit_code == 0
803+
call_args = mock_pumpswap.call_args
804+
assert call_args[0][5] == 5 or call_args.kwargs.get("slippage") == 5
805+
806+
807+
def test_sell_graduated_fallback_forwards_slippage(tmp_path, monkeypatch):
808+
"""--slippage 5 forwarded to sell_pumpswap on graduated fallback."""
809+
_setup_wallet(tmp_path, monkeypatch)
810+
811+
with (
812+
patch("pumpfun_cli.commands.trade.sell_token", new_callable=AsyncMock) as mock_sell,
813+
patch("pumpfun_cli.commands.trade.sell_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
814+
):
815+
mock_sell.return_value = {"error": "graduated", "message": "Token has graduated."}
816+
mock_pumpswap.return_value = {
817+
"action": "sell",
818+
"venue": "pumpswap",
819+
"mint": _FAKE_MINT,
820+
"sol_received": 0.01,
821+
"tokens_sold": 100.0,
822+
"signature": "sig",
823+
"explorer": "https://solscan.io/tx/sig",
824+
}
825+
826+
result = runner.invoke(
827+
app,
828+
["--json", "--rpc", "http://rpc", "sell", "--slippage", "5", _FAKE_MINT, "all"],
829+
)
830+
831+
assert result.exit_code == 0
832+
call_args = mock_pumpswap.call_args
833+
assert call_args[0][5] == 5 or call_args.kwargs.get("slippage") == 5
834+
835+
836+
def test_buy_graduated_fallback_forwards_priority_fee(tmp_path, monkeypatch):
837+
"""--priority-fee + --compute-units forwarded to buy_pumpswap on graduated fallback."""
838+
_setup_wallet(tmp_path, monkeypatch)
839+
840+
with (
841+
patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy,
842+
patch("pumpfun_cli.commands.trade.buy_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
843+
):
844+
mock_buy.return_value = {"error": "graduated", "message": "Token has graduated."}
845+
mock_pumpswap.return_value = {
846+
"action": "buy",
847+
"venue": "pumpswap",
848+
"mint": _FAKE_MINT,
849+
"sol_spent": 0.01,
850+
"tokens_received": 100.0,
851+
"signature": "sig",
852+
"explorer": "https://solscan.io/tx/sig",
853+
}
854+
855+
result = runner.invoke(
856+
app,
857+
[
858+
"--json",
859+
"--rpc",
860+
"http://rpc",
861+
"--priority-fee",
862+
"55000",
863+
"--compute-units",
864+
"350000",
865+
"buy",
866+
_FAKE_MINT,
867+
"0.01",
868+
],
869+
)
870+
871+
assert result.exit_code == 0
872+
call_kwargs = mock_pumpswap.call_args.kwargs
873+
assert call_kwargs.get("priority_fee") == 55000
874+
assert call_kwargs.get("compute_units") == 350000
875+
876+
877+
def test_buy_graduated_fallback_forwards_dry_run(tmp_path, monkeypatch):
878+
"""--dry-run forwarded to buy_pumpswap on graduated fallback."""
879+
_setup_wallet(tmp_path, monkeypatch)
880+
881+
with (
882+
patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy,
883+
patch("pumpfun_cli.commands.trade.buy_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
884+
):
885+
mock_buy.return_value = {"error": "graduated", "message": "Token has graduated."}
886+
mock_pumpswap.return_value = {
887+
"dry_run": True,
888+
"action": "buy",
889+
"venue": "pumpswap",
890+
"mint": _FAKE_MINT,
891+
"sol_in": 0.01,
892+
"expected_tokens": 100.0,
893+
"effective_price_sol": 0.0001,
894+
"spot_price_sol": 0.00009,
895+
"price_impact_pct": 1.0,
896+
"min_tokens_out": 95.0,
897+
"slippage_pct": 15,
898+
}
899+
900+
result = runner.invoke(
901+
app,
902+
["--json", "--rpc", "http://rpc", "buy", "--dry-run", _FAKE_MINT, "0.01"],
903+
)
904+
905+
assert result.exit_code == 0
906+
data = json.loads(result.output)
907+
assert data["dry_run"] is True
908+
assert data["venue"] == "pumpswap"
909+
call_kwargs = mock_pumpswap.call_args
910+
assert call_kwargs.kwargs.get("dry_run") is True or call_kwargs[1].get("dry_run") is True
911+
912+
913+
def test_sell_graduated_fallback_forwards_dry_run(tmp_path, monkeypatch):
914+
"""--dry-run forwarded to sell_pumpswap on graduated fallback."""
915+
_setup_wallet(tmp_path, monkeypatch)
916+
917+
with (
918+
patch("pumpfun_cli.commands.trade.sell_token", new_callable=AsyncMock) as mock_sell,
919+
patch("pumpfun_cli.commands.trade.sell_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
920+
):
921+
mock_sell.return_value = {"error": "graduated", "message": "Token has graduated."}
922+
mock_pumpswap.return_value = {
923+
"dry_run": True,
924+
"action": "sell",
925+
"venue": "pumpswap",
926+
"mint": _FAKE_MINT,
927+
"tokens_in": 100.0,
928+
"expected_sol": 0.01,
929+
"effective_price_sol": 0.0001,
930+
"spot_price_sol": 0.00009,
931+
"price_impact_pct": -1.0,
932+
"min_sol_out": 0.0085,
933+
"slippage_pct": 15,
934+
}
935+
936+
result = runner.invoke(
937+
app,
938+
["--json", "--rpc", "http://rpc", "sell", "--dry-run", _FAKE_MINT, "100"],
939+
)
940+
941+
assert result.exit_code == 0
942+
data = json.loads(result.output)
943+
assert data["dry_run"] is True
944+
assert data["venue"] == "pumpswap"
945+
946+
947+
def test_buy_graduated_fallback_forwards_confirm(tmp_path, monkeypatch):
948+
"""--confirm forwarded to buy_pumpswap on graduated fallback."""
949+
_setup_wallet(tmp_path, monkeypatch)
950+
951+
with (
952+
patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy,
953+
patch("pumpfun_cli.commands.trade.buy_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
954+
):
955+
mock_buy.return_value = {"error": "graduated", "message": "Token has graduated."}
956+
mock_pumpswap.return_value = {
957+
"action": "buy",
958+
"venue": "pumpswap",
959+
"mint": _FAKE_MINT,
960+
"sol_spent": 0.01,
961+
"tokens_received": 100.0,
962+
"signature": "sig",
963+
"explorer": "https://solscan.io/tx/sig",
964+
"confirmed": True,
965+
}
966+
967+
result = runner.invoke(
968+
app,
969+
["--json", "--rpc", "http://rpc", "buy", "--confirm", _FAKE_MINT, "0.01"],
970+
)
971+
972+
assert result.exit_code == 0
973+
call_kwargs = mock_pumpswap.call_args
974+
assert call_kwargs.kwargs.get("confirm") is True or call_kwargs[1].get("confirm") is True
975+
976+
977+
def test_buy_not_found_no_pumpswap_fallback(tmp_path, monkeypatch):
978+
"""not_found does NOT trigger pumpswap fallback."""
979+
_setup_wallet(tmp_path, monkeypatch)
980+
981+
with (
982+
patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy,
983+
patch("pumpfun_cli.commands.trade.buy_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
984+
):
985+
mock_buy.return_value = {"error": "not_found", "message": "No bonding curve found."}
986+
987+
result = runner.invoke(
988+
app,
989+
["--rpc", "http://rpc", "buy", _FAKE_MINT, "0.01"],
990+
)
991+
992+
assert result.exit_code != 0
993+
mock_pumpswap.assert_not_called()
994+
995+
996+
def test_sell_not_found_no_pumpswap_fallback(tmp_path, monkeypatch):
997+
"""not_found does NOT trigger pumpswap fallback for sell."""
998+
_setup_wallet(tmp_path, monkeypatch)
999+
1000+
with (
1001+
patch("pumpfun_cli.commands.trade.sell_token", new_callable=AsyncMock) as mock_sell,
1002+
patch("pumpfun_cli.commands.trade.sell_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
1003+
):
1004+
mock_sell.return_value = {"error": "not_found", "message": "No bonding curve found."}
1005+
1006+
result = runner.invoke(
1007+
app,
1008+
["--rpc", "http://rpc", "sell", _FAKE_MINT, "all"],
1009+
)
1010+
1011+
assert result.exit_code != 0
1012+
mock_pumpswap.assert_not_called()
1013+
1014+
1015+
def test_buy_graduated_fallback_pumpswap_error(tmp_path, monkeypatch):
1016+
"""pumpswap error surfaces correctly after graduated fallback."""
1017+
_setup_wallet(tmp_path, monkeypatch)
1018+
1019+
with (
1020+
patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy,
1021+
patch("pumpfun_cli.commands.trade.buy_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
1022+
):
1023+
mock_buy.return_value = {"error": "graduated", "message": "Token has graduated."}
1024+
mock_pumpswap.return_value = {
1025+
"error": "pumpswap_error",
1026+
"message": "No PumpSwap pool found for this token.",
1027+
}
1028+
1029+
result = runner.invoke(
1030+
app,
1031+
["--rpc", "http://rpc", "buy", _FAKE_MINT, "0.01"],
1032+
)
1033+
1034+
assert result.exit_code != 0
1035+
assert "PumpSwap" in result.output or "pumpswap" in result.output.lower()
1036+
1037+
1038+
def test_sell_graduated_fallback_sell_all(tmp_path, monkeypatch):
1039+
"""sell 'all' through graduated fallback works."""
1040+
_setup_wallet(tmp_path, monkeypatch)
1041+
1042+
with (
1043+
patch("pumpfun_cli.commands.trade.sell_token", new_callable=AsyncMock) as mock_sell,
1044+
patch("pumpfun_cli.commands.trade.sell_pumpswap", new_callable=AsyncMock) as mock_pumpswap,
1045+
):
1046+
mock_sell.return_value = {"error": "graduated", "message": "Token has graduated."}
1047+
mock_pumpswap.return_value = {
1048+
"action": "sell",
1049+
"venue": "pumpswap",
1050+
"mint": _FAKE_MINT,
1051+
"sol_received": 0.05,
1052+
"tokens_sold": 500.0,
1053+
"signature": "sellall_sig",
1054+
"explorer": "https://solscan.io/tx/sellall_sig",
1055+
}
1056+
1057+
result = runner.invoke(
1058+
app,
1059+
["--json", "--rpc", "http://rpc", "sell", _FAKE_MINT, "all"],
1060+
)
1061+
1062+
assert result.exit_code == 0
1063+
data = json.loads(result.output)
1064+
assert data["venue"] == "pumpswap"
1065+
assert data["tokens_sold"] == 500.0
1066+
# Verify "all" was passed through
1067+
call_args = mock_pumpswap.call_args
1068+
assert call_args[0][4] == "all"

0 commit comments

Comments
 (0)