Skip to content

Commit 8f81e54

Browse files
[S3-L1CLI] L1-CLI gate: test_cli_build_target_all_36_chains + KNOWN_BROKEN ledger
Why this exists: S3-A2 (Tron) and S3-A3 (AvaX) were both reverted (commits 9e7cb22, 436e1d0) because their L1 unit tests and self-authored L3 e2e_smoke siblings exercised only the adapter Python classes directly — never the real production entrypoint (tools/chain_adapters/cli.py build-target, called from target_generator.sh). Both adapters had P0 bugs at the CLI layer that L1 PASS could not surface. This commit closes that gap: every chain registered in config/chains/*.json must produce a valid vegeta target via the real cli.py subprocess path, with the supplied address appearing in either the decoded JSON body or the URL. What's added: - tests/test_chain_adapters.py: new test_cli_build_target_all_36_chains - KNOWN_BROKEN_CLI dict: 16 chains currently failing the CLI gate, each tagged with failure-mode (F1/F2/F3/F4) and fix-wave owner (S3-B/C/D/E/F) - Invariant: KNOWN_BROKEN set may only shrink across commits. Test fails on EITHER (a) new chain broken not in KNOWN_BROKEN (regression) OR (b) chain newly healthy but still in KNOWN_BROKEN (must remove entry). Current baseline (commit 436e1d0, 2026-05-24 audit): Healthy at CLI entrypoint: 20/36 Known broken: 16/36 F1 (chain_template.rpc_methods.single picked health-probe instead of benchmark method — pure template edit fix): 7 chains algorand, aptos, hedera, tezos, ton, kusama, polkadot F2 (REST path wrapped as jsonrpc body — adapter dispatch fix in tendermint/ogmios adapter): 5 chains celestia, injective, osmosis, cosmos-hub, cardano F3 (family mismatch: substrate/tendermint marked but chain runs EVM compat — family + method choice fix): 4 chains astar, moonbeam, sei, acala Test suite: 10/10 PASS (was 9/9 before this commit, no regression on existing tests). Fix-wave assignments (each wave must remove its assigned chains from KNOWN_BROKEN_CLI before its commit): S3-B (Tendermint 5 chains): celestia, injective, osmosis, cosmos-hub, sei S3-C (Substrate 5 chains): astar, moonbeam, acala, kusama, polkadot S3-E (REST 5 chains): algorand, aptos, hedera, tezos, ton S3-F (Ogmios 1 chain): cardano S3-A2' and S3-A3' (Tron + AvaX redo) will also be guarded by this gate, but since those chains are currently healthy under the baseline jsonrpc family (no entries in KNOWN_BROKEN), the gate enforces no regression rather than mandatory fix.
1 parent 436e1d0 commit 8f81e54

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

tests/test_chain_adapters.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,160 @@ def test_evm_compat_5chains_standard_enum():
310310
_ok(f"{chain}: {len(pf)} formats all standard")
311311

312312

313+
# ─────────────────────────────────────────────────────────────────────────────
314+
# Test 10: L1-CLI end-to-end — every chain must produce a valid vegeta target
315+
# via the real production entrypoint (tools/chain_adapters/cli.py build-target),
316+
# which is the only path called by target_generator.sh -> master_qps_executor.sh.
317+
#
318+
# Two assertions per chain:
319+
# (a) cli.py exits 0
320+
# (b) decoded body contains the supplied address
321+
#
322+
# KNOWN_BROKEN: chains where the CURRENT state is broken at the CLI entrypoint
323+
# (commit 436e1d0 baseline, 2026-05-24 audit). Each entry MUST cite its
324+
# failure-mode bucket (F1/F2/F3/F4) so the responsible S3 wave knows what to fix.
325+
#
326+
# Invariant: KNOWN_BROKEN must shrink monotonically. New chains may not be added
327+
# without explicit user decision. Each S3-B/C/D/E/F wave is required to remove
328+
# at least the entries assigned to it (see "Fix wave" column below).
329+
# ─────────────────────────────────────────────────────────────────────────────
330+
331+
# (chain, expected_failure_mode, fix_wave_owner, reason)
332+
KNOWN_BROKEN_CLI = {
333+
# F1: rpc_methods.single picked a health-probe (no address) instead of
334+
# a real benchmark method. Fix = pick a method from param_formats
335+
# that takes an address. Pure chain-template edit, no adapter work.
336+
"algorand": ("F1", "S3-E", "single='GET /v2/status' is health-probe; use 'GET /v2/accounts/{address}'"),
337+
"aptos": ("F1", "S3-E", "single='GET /v1' returns collection root; use 'POST /v1/view' or 'GET /v1/accounts/{addr}'"),
338+
"hedera": ("F1", "S3-E", "single='mirror_account_query' is logical name, no real path; use 'mirror_balance_query' or 'eth_getBalance'"),
339+
"tezos": ("F1", "S3-E", "single='GET /chains/main/blocks/head/header' has no address; use '/contracts/{addr}/balance'"),
340+
"ton": ("F1", "S3-E", "single='getMasterchainInfo' is health-probe; use 'getAddressBalance'"),
341+
"kusama": ("F1", "S3-C", "single='chain_getHeader' has no address; use 'system_account' or similar"),
342+
"polkadot": ("F1", "S3-C", "single='chain_getHeader' has no address; mixed_first 'GET /accounts/{addr}/balance-info' is correct shape"),
343+
344+
# F2: chain template uses REST paths in rpc_methods.single but
345+
# family=tendermint goes through jsonrpc.py generic builder which
346+
# wraps everything in {jsonrpc:"2.0", method:"<path>", params:{}}.
347+
# Fix = adapter dispatch (tendermint must do HTTP GET <path>, not POST JSON).
348+
"celestia": ("F2", "S3-B", "REST path '/status' wrapped as jsonrpc body; tendermint adapter must HTTP GET"),
349+
"injective": ("F2", "S3-B", "REST path '/status' wrapped as jsonrpc body"),
350+
"osmosis": ("F2", "S3-B", "REST path '/status' wrapped as jsonrpc body"),
351+
"cosmos-hub": ("F2", "S3-B", "REST path wrapped as jsonrpc body"),
352+
"cardano": ("F2", "S3-F", "ogmios family wraps 'GET /tip' as jsonrpc body; ogmios adapter is WebSocket JSON-RPC, different protocol"),
353+
354+
# F3: family=substrate but chain runs EVM via Frontier pallet. mixed_first
355+
# is eth_chainId (no address). Fix = decide family (substrate vs jsonrpc)
356+
# and pick benchmark method with address (eth_getBalance).
357+
"astar": ("F3", "S3-C", "family=substrate but Astar runs EVM via Frontier; single='eth_chainId' has no address"),
358+
"moonbeam": ("F3", "S3-C", "family=substrate but Moonbeam runs EVM via Frontier; single='eth_chainId' has no address"),
359+
"sei": ("F3", "S3-B", "family=tendermint with EVM compat layer; single='eth_chainId' has no address"),
360+
"acala": ("F3", "S3-C", "family=substrate; single='system_chain' has no address; pick eth_getBalance from param_formats"),
361+
}
362+
363+
assert len(KNOWN_BROKEN_CLI) == 16, f"KNOWN_BROKEN_CLI must have exactly 16 entries (commit 436e1d0 baseline), got {len(KNOWN_BROKEN_CLI)}"
364+
365+
366+
def _sample_address_for(family: str) -> str:
367+
"""Return a family-appropriate sample address for the build-target probe.
368+
369+
Adapters either echo the address verbatim into the body (so any string works
370+
for the L1 assertion) or template it into a URL path (REST {address}/{addr}).
371+
These canonical samples are recognizable in decoded bodies.
372+
"""
373+
return {
374+
"jsonrpc": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
375+
"bitcoin_jsonrpc": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
376+
"substrate": "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU",
377+
"tendermint": "cosmos1abc",
378+
"ogmios": "addr1q9adlx6mh0dr8xs0gpcm9nz5pqe5w2hzfx5l8qj5",
379+
"rest": "TESTADDR123",
380+
}.get(family, "TESTADDR123")
381+
382+
383+
def test_cli_build_target_all_36_chains():
384+
"""L1-CLI: every chain produces a valid vegeta target via cli.py with address.
385+
386+
Real production path: target_generator.sh:74 invokes this exact cli.py
387+
incantation. If this test passes, the CLI path is healthy; L1 PASS via
388+
direct adapter calls (test_3 / test_8 etc.) is NOT sufficient because
389+
cli.py adds an additional layer of plumbing (param_format lookup from
390+
chain template, BLOCKCHAIN_NODE env, sys.path manipulation).
391+
"""
392+
print("\n[10] CLI build-target end-to-end for all 36 chains (real production path)")
393+
chains_dir = REPO / "config" / "chains"
394+
cli_py = REPO / "tools" / "chain_adapters" / "cli.py"
395+
396+
expected_broken = set(KNOWN_BROKEN_CLI.keys())
397+
actually_broken: set[str] = set()
398+
healthy: list[str] = []
399+
400+
for cf in sorted(chains_dir.glob("*.json")):
401+
chain = cf.stem
402+
tpl = json.loads(cf.read_text())
403+
family = tpl.get("_meta", {}).get("adapter_family", "")
404+
single_method = (tpl.get("rpc_methods") or {}).get("single")
405+
if not single_method:
406+
actually_broken.add(chain)
407+
continue
408+
409+
addr = _sample_address_for(family)
410+
r = subprocess.run(
411+
["python3", str(cli_py), "build-target",
412+
"--chain", chain, "--method", single_method,
413+
"--address", addr, "--rpc-url", "http://localhost:8545"],
414+
capture_output=True, text=True, timeout=10,
415+
)
416+
417+
# Gate 1: exit code
418+
if r.returncode != 0:
419+
actually_broken.add(chain)
420+
continue
421+
# Gate 2: body is valid JSON
422+
try:
423+
tgt = json.loads(r.stdout)
424+
body = base64.b64decode(tgt["body"]).decode("utf-8", errors="replace")
425+
except Exception:
426+
actually_broken.add(chain)
427+
continue
428+
# Gate 3: body or URL contains the address we passed
429+
url = tgt.get("url", "")
430+
if addr not in body and addr not in url:
431+
actually_broken.add(chain)
432+
continue
433+
434+
healthy.append(chain)
435+
436+
# Invariant check: actually_broken must be a SUBSET of expected_broken
437+
# (broken set may only shrink, never grow). New chains added to the
438+
# framework must either pass cleanly or be explicitly added to
439+
# KNOWN_BROKEN_CLI with a fix-wave owner.
440+
unexpected_new_broken = actually_broken - expected_broken
441+
unexpectedly_healthy = expected_broken - actually_broken
442+
443+
print(f" Healthy: {len(healthy)}/36 chains")
444+
print(f" KNOWN_BROKEN (must shrink, never grow): {len(expected_broken)}")
445+
print(f" Actually broken now: {len(actually_broken)}")
446+
447+
if unexpected_new_broken:
448+
_fail(
449+
f"REGRESSION — new chains broken at CLI entrypoint that were "
450+
f"NOT in KNOWN_BROKEN_CLI: {sorted(unexpected_new_broken)}. "
451+
f"Either fix them or add to KNOWN_BROKEN_CLI with explicit "
452+
f"fix-wave assignment (F1/F2/F3/F4 + S3-B/C/D/E/F owner)."
453+
)
454+
455+
if unexpectedly_healthy:
456+
# This is GOOD news — chain got fixed. But the test must enforce the
457+
# KNOWN_BROKEN list is current, so author must remove the entry.
458+
_fail(
459+
f"PROGRESS — these chains are now healthy and must be REMOVED "
460+
f"from KNOWN_BROKEN_CLI: {sorted(unexpectedly_healthy)}. "
461+
f"Edit tests/test_chain_adapters.py KNOWN_BROKEN_CLI dict."
462+
)
463+
464+
_ok(f"{len(healthy)}/36 healthy + {len(actually_broken)} known-broken (set matches expected)")
465+
466+
313467
# ─────────────────────────────────────────────────────────────────────────────
314468
# Main
315469
# ─────────────────────────────────────────────────────────────────────────────
@@ -324,6 +478,7 @@ def main():
324478
test_rest_requires_env_and_path_map,
325479
test_jsonrpc_s3a_new_formats,
326480
test_evm_compat_5chains_standard_enum,
481+
test_cli_build_target_all_36_chains,
327482
]
328483
print(f"Running {len(tests)} test groups for chain_adapters")
329484
for t in tests:

0 commit comments

Comments
 (0)