From 87991bf2489faa05bd94b3b51153a8aa2e42ea60 Mon Sep 17 00:00:00 2001 From: "emlautarom1-agent[bot]" <292495798+emlautarom1-agent[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:47:57 -0300 Subject: [PATCH 1/3] fix(testutil): emit inclusion flags on the simnet block endpoint `GET /eth/v2/beacon/blocks/{block_id}` returned a `{version, data}` envelope, so `get_block_v2` failed to decode and the inclusion checker warned once per due slot. The beacon-API spec marks `execution_optimistic` and `finalized` required and non-nullable, and every major client emits both, so the generated client requires them too. --- crates/testutil/src/beaconmock/defaults.rs | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/testutil/src/beaconmock/defaults.rs b/crates/testutil/src/beaconmock/defaults.rs index 1f6114e7..a0500da6 100644 --- a/crates/testutil/src/beaconmock/defaults.rs +++ b/crates/testutil/src/beaconmock/defaults.rs @@ -601,6 +601,8 @@ fn bellatrix_signed_block_response() -> Value { json!({ "version": "bellatrix", + "execution_optimistic": false, + "finalized": false, "data": { "message": { "slot": random_slot().to_string(), @@ -778,6 +780,10 @@ pub(crate) fn default_genesis_time() -> DateTime { mod tests { use super::*; use crate::beaconmock::BeaconMock; + use pluto_eth2api::{ + client::EthBeaconNodeApiClient, + types::{GetBlockV2Request, GetBlockV2Response}, + }; #[test] fn default_spec_contains_load_bearing_keys() { @@ -837,4 +843,28 @@ mod tests { .expect("blocks request (numeric)"); assert_eq!(resp.status(), 200); } + + /// The inclusion checker consumes this endpoint through the generated + /// client, which requires `execution_optimistic` and `finalized` — the + /// beacon-API spec marks both required and non-nullable. Asserting on the + /// raw body is not enough: only a decode proves the mock is consumable. + #[tokio::test] + async fn bellatrix_signed_block_decodes_through_the_generated_client() { + let mock = BeaconMock::builder() + .build() + .await + .expect("build beacon mock"); + + let client = EthBeaconNodeApiClient::with_base_url(mock.uri()).expect("client"); + let request = GetBlockV2Request::builder() + .block_id("123".to_string()) + .build() + .expect("block request"); + + let response = client.get_block_v2(request).await.expect("get_block_v2"); + assert!( + matches!(response, GetBlockV2Response::Ok(_)), + "expected a decoded 200, got {response:?}" + ); + } } From 192373fa1fffca06d82884e00e68e12fe05e966b Mon Sep 17 00:00:00 2001 From: "emlautarom1-agent[bot]" <292495798+emlautarom1-agent[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:16:45 -0300 Subject: [PATCH 2/3] fix(testutil): mount the node peer count endpoint on the simnet mock The readiness checker polls `GET /eth/v1/node/peer_count` once a minute. Unmounted, it warned on every poll and left `app_beacon_node_peers` at a flat zero, indistinguishable from a beacon node with no peers. --- crates/testutil/src/beaconmock/defaults.rs | 42 +++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/testutil/src/beaconmock/defaults.rs b/crates/testutil/src/beaconmock/defaults.rs index a0500da6..746beaf6 100644 --- a/crates/testutil/src/beaconmock/defaults.rs +++ b/crates/testutil/src/beaconmock/defaults.rs @@ -91,6 +91,21 @@ pub(crate) async fn mount_defaults(server: &MockServer, state: Arc) { }) .await; + // Polled once a minute by the readiness checker (`run_ready_checker`). + // `connected` must stay non-zero: zero peers is a readiness failure + // (`ReadinessError::BeaconNodeZeroPeers`). 80 matches charon's beaconmock. + mount_json(server, "GET", "/eth/v1/node/peer_count", |_| { + json!({ + "data": { + "connected": "80", + "connecting": "0", + "disconnected": "0", + "disconnecting": "0" + } + }) + }) + .await; + mount_json(server, "GET", "/eth/v1/beacon/headers/head", |_| { json!({ "data": { @@ -782,7 +797,7 @@ mod tests { use crate::beaconmock::BeaconMock; use pluto_eth2api::{ client::EthBeaconNodeApiClient, - types::{GetBlockV2Request, GetBlockV2Response}, + types::{GetBlockV2Request, GetBlockV2Response, GetPeerCountRequest, GetPeerCountResponse}, }; #[test] @@ -867,4 +882,29 @@ mod tests { "expected a decoded 200, got {response:?}" ); } + + /// The readiness checker polls this every minute and treats zero connected + /// peers as unready, so an unmounted route both warns and pins + /// `app_beacon_node_peers` to a misleading zero. + #[tokio::test] + async fn peer_count_reports_connected_peers() { + let mock = BeaconMock::builder() + .build() + .await + .expect("build beacon mock"); + + let client = EthBeaconNodeApiClient::with_base_url(mock.uri()).expect("client"); + + let response = client + .get_peer_count(GetPeerCountRequest {}) + .await + .expect("get_peer_count"); + let GetPeerCountResponse::Ok(peers) = response else { + panic!("expected a decoded 200, got {response:?}"); + }; + assert_ne!( + peers.data.connected, "0", + "zero connected peers fails the readiness check" + ); + } } From 73d2f84b21338e047be344f884f14c76af5409ee Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 6 Aug 2026 10:18:50 -0300 Subject: [PATCH 3/3] fix(tests): refactor block endpoint tests for clarity and consistency --- crates/testutil/src/beaconmock/defaults.rs | 88 +++++++--------------- 1 file changed, 29 insertions(+), 59 deletions(-) diff --git a/crates/testutil/src/beaconmock/defaults.rs b/crates/testutil/src/beaconmock/defaults.rs index 746beaf6..283eb4fb 100644 --- a/crates/testutil/src/beaconmock/defaults.rs +++ b/crates/testutil/src/beaconmock/defaults.rs @@ -795,9 +795,9 @@ pub(crate) fn default_genesis_time() -> DateTime { mod tests { use super::*; use crate::beaconmock::BeaconMock; - use pluto_eth2api::{ - client::EthBeaconNodeApiClient, - types::{GetBlockV2Request, GetBlockV2Response, GetPeerCountRequest, GetPeerCountResponse}, + use pluto_eth2api::types::{ + ConsensusVersion, GetBlockV2Request, GetBlockV2Response, GetPeerCountRequest, + GetPeerCountResponse, }; #[test] @@ -820,67 +820,32 @@ mod tests { } } - #[tokio::test] - async fn bellatrix_signed_block_endpoint_returns_versioned_block() { - let mock = BeaconMock::builder() - .build() - .await - .expect("build beacon mock"); - - let base = mock.uri(); - let http = reqwest::Client::new(); - - // The `block_id` segment is opaque to the mock; "head" exercises the - // path_regex match. - let resp = http - .get(format!("{base}/eth/v2/beacon/blocks/head")) - .send() - .await - .expect("blocks request"); - assert_eq!(resp.status(), 200, "blocks endpoint should succeed"); - - let body: Value = resp.json().await.expect("blocks json"); - assert_eq!( - body.get("version").and_then(Value::as_str), - Some("bellatrix"), - "version field should be bellatrix" - ); - assert!( - body.get("data").and_then(Value::as_object).is_some(), - "data field should be a JSON object" - ); - - // Same endpoint should also match a numeric block_id. - let resp = http - .get(format!("{base}/eth/v2/beacon/blocks/123")) - .send() - .await - .expect("blocks request (numeric)"); - assert_eq!(resp.status(), 200); - } - /// The inclusion checker consumes this endpoint through the generated /// client, which requires `execution_optimistic` and `finalized` — the - /// beacon-API spec marks both required and non-nullable. Asserting on the - /// raw body is not enough: only a decode proves the mock is consumable. + /// beacon-API spec marks both required and non-nullable. #[tokio::test] - async fn bellatrix_signed_block_decodes_through_the_generated_client() { + async fn block_endpoint_serves_a_decodable_bellatrix_block() { let mock = BeaconMock::builder() .build() .await .expect("build beacon mock"); - let client = EthBeaconNodeApiClient::with_base_url(mock.uri()).expect("client"); - let request = GetBlockV2Request::builder() - .block_id("123".to_string()) - .build() - .expect("block request"); - - let response = client.get_block_v2(request).await.expect("get_block_v2"); - assert!( - matches!(response, GetBlockV2Response::Ok(_)), - "expected a decoded 200, got {response:?}" - ); + let client = mock.client(); + + // The `block_id` segment is opaque to the mock; "head" and a numeric + // id both exercise the path_regex match. + for block_id in ["head", "123"] { + let request = GetBlockV2Request::builder() + .block_id(block_id.to_string()) + .build() + .expect("block request"); + + let response = client.get_block_v2(request).await.expect("get_block_v2"); + let GetBlockV2Response::Ok(block) = response else { + panic!("expected a decoded 200 for {block_id}, got {response:?}"); + }; + assert_eq!(block.version, ConsensusVersion::Bellatrix); + } } /// The readiness checker polls this every minute and treats zero connected @@ -893,7 +858,7 @@ mod tests { .await .expect("build beacon mock"); - let client = EthBeaconNodeApiClient::with_base_url(mock.uri()).expect("client"); + let client = mock.client(); let response = client .get_peer_count(GetPeerCountRequest {}) @@ -902,8 +867,13 @@ mod tests { let GetPeerCountResponse::Ok(peers) = response else { panic!("expected a decoded 200, got {response:?}"); }; - assert_ne!( - peers.data.connected, "0", + let connected_peers: u64 = peers + .data + .connected + .parse() + .expect("connected parses as u64"); + assert!( + connected_peers > 0, "zero connected peers fails the readiness check" ); }