From b216621d866d7da513d618f3d8761618c50c8bf1 Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 3 Sep 2026 15:36:27 +0100 Subject: [PATCH] fix: preserve runtime network identity --- CLAUDE.md | 3 +- README.md | 11 ++++- docs/api-references/index.md | 6 ++- docs/studio-runner.md | 13 ++++-- gltest/fees/profile.py | 18 ++++++-- gltest_cli/config/constants.py | 10 ++++- gltest_cli/config/plugin.py | 1 + gltest_cli/config/types.py | 40 +++++++++++++++-- gltest_cli/config/user.py | 20 ++++++++- pyproject.toml | 2 +- tests/gltest/fees/test_fee_profile.py | 10 ++++- .../config/test_config_integration.py | 5 +++ .../config/test_fee_profile_plugin.py | 30 +++++++++++++ .../gltest_cli/config/test_general_config.py | 43 +++++++++++++++++++ tests/gltest_cli/config/test_plugin.py | 4 +- tests/gltest_cli/config/test_user.py | 5 +++ uv.lock | 8 ++-- 17 files changed, 203 insertions(+), 26 deletions(-) create mode 100644 tests/gltest_cli/config/test_fee_profile_plugin.py diff --git a/CLAUDE.md b/CLAUDE.md index acb791f..cbb06fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,7 +145,8 @@ print(stats) # Shows success rate, reliability score, execution time, etc. ### Network Configuration Networks are configured in `gltest.config.yaml`: -- `localnet`: Local development (chain ID 61999) +- `localnet`: Local development (chain ID 61127) +- `studio_devnet`: Hosted Studio preview (chain ID 61997) - `testnet_asimov`: Public testnet (chain ID 4221) Paths can be configured in `gltest.config.yaml`: diff --git a/README.md b/README.md index 7414796..e2f214b 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ Run with the `gltest` CLI: ```bash gltest # Run all tests gltest tests/test_mycontract.py # Specific file +gltest --network studio_devnet # Hosted release preview gltest --network studionet # Specific network gltest --leader-only # Skip consensus (faster) gltest -v # Verbose output @@ -152,6 +153,9 @@ networks: url: "http://127.0.0.1:4000/api" leader_only: false + studio_devnet: + # Pre-configured release preview — accounts auto-generated + studionet: # Pre-configured — accounts auto-generated @@ -169,7 +173,7 @@ environment: .env ``` Key options: -- **Networks**: `localnet` and `studionet` work out of the box. `testnet_asimov` requires account keys. +- **Networks**: `localnet`, `studio_devnet`, and `studionet` work out of the box. `testnet_asimov` requires account keys. - **Paths**: Where your contracts and artifacts live. - **Environment**: `.env` file for private keys. @@ -241,6 +245,7 @@ than a consumed fee amount. { "version": 1, "network": "localnet", + "chainId": 61127, "measuredAt": "2026-06-10T12:00:00Z", "deploy": { "leaderTimeunitsAllocation": "125", @@ -266,7 +271,9 @@ include consumed fee data. Testnet receipts do not expose consumed fees yet, and direct/sim mode does not go through these receipt paths. Time-unit allocations are recorded from the submitted fee distribution when the backend receipt includes it. Live price caps and `feeValue` are intentionally omitted so the SDK -can quote them from the current network policy at transaction time. +can quote them from the current network policy at transaction time. The numeric +`chainId` scopes the measurements to the exact selected runtime chain; consumers +must fall back to network defaults when it is absent or does not match. ### Assertions diff --git a/docs/api-references/index.md b/docs/api-references/index.md index 445584b..54cb0de 100644 --- a/docs/api-references/index.md +++ b/docs/api-references/index.md @@ -130,6 +130,7 @@ Run with the `gltest` CLI: ```bash gltest # Run all tests gltest tests/test_mycontract.py # Specific file +gltest --network studio_devnet # Hosted release preview gltest --network studionet # Specific network gltest --leader-only # Skip consensus (faster) gltest -v # Verbose output @@ -147,6 +148,9 @@ networks: url: "http://127.0.0.1:4000/api" leader_only: false + studio_devnet: + # Pre-configured release preview — accounts auto-generated + studionet: # Pre-configured — accounts auto-generated @@ -164,7 +168,7 @@ environment: .env ``` Key options: -- **Networks**: `localnet` and `studionet` work out of the box. `testnet_asimov` requires account keys. +- **Networks**: `localnet`, `studio_devnet`, and `studionet` work out of the box. `testnet_asimov` requires account keys. - **Paths**: Where your contracts and artifacts live. - **Environment**: `.env` file for private keys. diff --git a/docs/studio-runner.md b/docs/studio-runner.md index 0e0b267..f057348 100644 --- a/docs/studio-runner.md +++ b/docs/studio-runner.md @@ -7,7 +7,7 @@ Deploy and test intelligent contracts against a running GenLayer Studio instance Studio mode deploys contracts to GenLayer Studio (or a remote network) and interacts through JSON-RPC. Every write operation goes through the full consensus pipeline. - **Fidelity**: Real consensus, real validators, real transaction lifecycle -- **Networks**: localnet (Docker), studionet, testnet_asimov, or custom +- **Networks**: localnet (Docker), Studio Devnet, studionet, public testnets, or custom - **Mocking**: Mock validators with predefined LLM/web responses for deterministic tests ### When to Use Studio Mode @@ -74,6 +74,9 @@ networks: url: "http://127.0.0.1:4000/api" leader_only: false + studio_devnet: + # Pre-configured release preview — accounts auto-generated + studionet: # Pre-configured — accounts auto-generated # Override any settings if needed @@ -107,8 +110,9 @@ environment: .env # File containing private keys and secrets | Network | Accounts | Notes | |---------|----------|-------| -| `localnet` | Auto-generated | Local Docker deployment | -| `studionet` | Auto-generated | GenLayer Studio cloud | +| `localnet` | Auto-generated | Local Docker deployment (chain 61127) | +| `studio_devnet` | Auto-generated | Hosted release preview (chain 61997) | +| `studionet` | Auto-generated | Stable GenLayer Studio cloud (chain 61999) | | `testnet_asimov` | Must be configured | Public testnet | **Network configuration fields:** @@ -117,7 +121,7 @@ environment: .env # File containing private keys and secrets |-------|----------|-------------| | `url` | For custom networks | RPC endpoint | | `id` | For custom networks | Chain ID | -| `chain_type` | For custom networks | One of: `localnet`, `studionet`, `testnet_asimov` | +| `chain_type` | For custom networks | One of the pre-configured network types above | | `accounts` | For testnet/custom | List of private keys (use env vars) | | `from` | No | Which account to use as default (defaults to first) | | `leader_only` | No | Skip consensus for all operations | @@ -163,6 +167,7 @@ gltest -v # Verbose output ```bash gltest --network localnet # Specific network (from config) +gltest --network studio_devnet gltest --network studionet gltest --network testnet_asimov gltest --rpc-url http://custom:4000/api # Custom RPC URL diff --git a/gltest/fees/profile.py b/gltest/fees/profile.py index 766b7b7..ebb8968 100644 --- a/gltest/fees/profile.py +++ b/gltest/fees/profile.py @@ -42,12 +42,16 @@ def record_method(self, method_name: str, receipt: Dict[str, Any]) -> None: method_values = self._methods.setdefault(method_name, {}) self._record_max(method_values, observation) - def build_profile(self, network: str, headroom: float) -> Dict[str, Any]: + def build_profile( + self, network: str, headroom: float, chain_id: Optional[int] = None + ) -> Dict[str, Any]: profile: Dict[str, Any] = { "version": 1, "network": network, "measuredAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } + if chain_id is not None: + profile["chainId"] = chain_id if self._deploy: profile["deploy"] = self._apply_headroom(self._deploy, headroom) profile["methods"] = { @@ -56,8 +60,16 @@ def build_profile(self, network: str, headroom: float) -> Dict[str, Any]: } return profile - def write(self, path: Path, network: str, headroom: float) -> Dict[str, Any]: - profile = self.build_profile(network=network, headroom=headroom) + def write( + self, + path: Path, + network: str, + headroom: float, + chain_id: Optional[int] = None, + ) -> Dict[str, Any]: + profile = self.build_profile( + network=network, headroom=headroom, chain_id=chain_id + ) path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(profile, indent=2) + "\n", encoding="utf-8") diff --git a/gltest_cli/config/constants.py b/gltest_cli/config/constants.py index 8316567..c10fb26 100644 --- a/gltest_cli/config/constants.py +++ b/gltest_cli/config/constants.py @@ -3,8 +3,14 @@ GLTEST_CONFIG_FILE = "gltest.config.yaml" DEFAULT_NETWORK = "localnet" -PRECONFIGURED_NETWORKS = ["localnet", "studionet", "testnet_asimov", "testnet_bradbury"] -CHAINS = ["localnet", "studionet", "testnet_asimov", "testnet_bradbury"] +PRECONFIGURED_NETWORKS = [ + "localnet", + "studio_devnet", + "studionet", + "testnet_asimov", + "testnet_bradbury", +] +CHAINS = PRECONFIGURED_NETWORKS.copy() DEFAULT_RPC_URL = SIMULATOR_JSON_RPC_URL DEFAULT_ENVIRONMENT = ".env" DEFAULT_CONTRACTS_DIR = Path("contracts") diff --git a/gltest_cli/config/plugin.py b/gltest_cli/config/plugin.py index 90d2f6a..49286d2 100644 --- a/gltest_cli/config/plugin.py +++ b/gltest_cli/config/plugin.py @@ -222,6 +222,7 @@ def pytest_sessionfinish(session, exitstatus): path=fee_profile_path, network=general_config.get_network_name(), headroom=general_config.get_fee_profile_headroom(), + chain_id=general_config.get_chain().id, ) logger.info(f"Wrote fee profile to {fee_profile_path}") if not collector.has_observations(): diff --git a/gltest_cli/config/types.py b/gltest_cli/config/types.py index 9438b0d..cd64d90 100644 --- a/gltest_cli/config/types.py +++ b/gltest_cli/config/types.py @@ -1,7 +1,14 @@ +from copy import deepcopy from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional -from genlayer_py.chains import localnet, studionet, testnet_asimov, testnet_bradbury +from genlayer_py.chains import ( + localnet, + studio_devnet, + studionet, + testnet_asimov, + testnet_bradbury, +) from genlayer_py.types import GenLayerChain from gltest_cli.config.constants import PRECONFIGURED_NETWORKS from gltest_cli.config.constants import ( @@ -190,12 +197,39 @@ def get_chain_type(self) -> str: def get_chain(self) -> GenLayerChain: chain_map = { "localnet": localnet, + "studio_devnet": studio_devnet, "studionet": studionet, "testnet_asimov": testnet_asimov, "testnet_bradbury": testnet_bradbury, } chain_type = self.get_chain_type() - return chain_map[chain_type] + chain = deepcopy(chain_map[chain_type]) + + # The selected network is the runtime authority. This is especially + # important for custom Studio deployments: the endpoint and chain ID + # must travel together or signed transactions use the preset's ID. + network_config = self.user_config.networks.get(self.get_network_name()) + uses_network_chain_type = ( + self.plugin_config.chain_type is None + or ( + network_config is not None + and self.plugin_config.chain_type == network_config.chain_type + ) + ) + if ( + uses_network_chain_type + and network_config is not None + and network_config.id is not None + ): + chain.id = network_config.id + rpc_url = ( + self.get_rpc_url() + if uses_network_chain_type or self.plugin_config.rpc_url is not None + else None + ) + if rpc_url: + chain.rpc_urls["default"]["http"] = [rpc_url] + return chain def get_default_wait_interval(self) -> int: if self.plugin_config.default_wait_interval is not None: @@ -243,4 +277,4 @@ def check_local_rpc(self) -> bool: return self.get_chain_type() == "localnet" def check_studio_based_rpc(self) -> bool: - return self.get_chain_type() in ("studionet", "localnet") + return self.get_chain_type() in ("studio_devnet", "studionet", "localnet") diff --git a/gltest_cli/config/user.py b/gltest_cli/config/user.py index 2c8beb8..74fc090 100644 --- a/gltest_cli/config/user.py +++ b/gltest_cli/config/user.py @@ -18,7 +18,13 @@ DEFAULT_LEADER_ONLY, CHAINS, ) -from genlayer_py.chains import localnet, studionet, testnet_asimov, testnet_bradbury +from genlayer_py.chains import ( + localnet, + studio_devnet, + studionet, + testnet_asimov, + testnet_bradbury, +) from gltest_cli.config.types import UserConfig, NetworkConfigData, PathConfig VALID_ROOT_KEYS = ["networks", "paths", "environment"] @@ -51,6 +57,16 @@ def get_default_user_config() -> UserConfig: default_wait_retries=DEFAULT_WAIT_RETRIES, chain_type="localnet", ), + "studio_devnet": NetworkConfigData( + id=studio_devnet.id, + url=studio_devnet.rpc_urls["default"]["http"][0], + accounts=accounts_private_keys, + from_account=accounts_private_keys[0], + leader_only=DEFAULT_LEADER_ONLY, + default_wait_interval=DEFAULT_WAIT_INTERVAL, + default_wait_retries=DEFAULT_WAIT_RETRIES, + chain_type="studio_devnet", + ), "studionet": NetworkConfigData( id=studionet.id, url=studionet.rpc_urls["default"]["http"][0], @@ -275,6 +291,8 @@ def _get_overridden_networks(raw_config: dict) -> tuple[dict, str]: if "url" in network_config: networks_config[network_name].url = network_config["url"] + if "id" in network_config: + networks_config[network_name].id = network_config["id"] if "accounts" in network_config: networks_config[network_name].accounts = network_config["accounts"] networks_config[network_name].from_account = network_config["accounts"][ diff --git a/pyproject.toml b/pyproject.toml index 1b58363..2668d0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "pytest", - "genlayer-py>=0.19.0rc1,<0.20.0", + "genlayer-py>=0.19.0rc2,<0.20.0", "colorama>=0.4.6", "pyyaml", "python-dotenv" diff --git a/tests/gltest/fees/test_fee_profile.py b/tests/gltest/fees/test_fee_profile.py index 93eaad6..f787216 100644 --- a/tests/gltest/fees/test_fee_profile.py +++ b/tests/gltest/fees/test_fee_profile.py @@ -116,10 +116,13 @@ def test_collector_records_max_and_applies_headroom_with_big_int(): collector.record_deploy(fee_receipt(101, 10)) collector.record_method("create_bet", fee_receipt(big_value, 0)) - profile = collector.build_profile(network="localnet", headroom=1.25) + profile = collector.build_profile( + network="localnet", headroom=1.25, chain_id=61127 + ) assert profile["version"] == 1 assert profile["network"] == "localnet" + assert profile["chainId"] == 61127 assert profile["deploy"] == { "leaderTimeunitsAllocation": "125", "validatorTimeunitsAllocation": "250", @@ -257,10 +260,13 @@ def test_write_creates_parent_dirs_and_round_trips_json(tmp_path): collector.record_deploy(fee_receipt(10, 0)) output_path = tmp_path / "profiles" / "fees.json" - profile = collector.write(output_path, network="localnet", headroom=1.25) + profile = collector.write( + output_path, network="localnet", headroom=1.25, chain_id=61127 + ) assert output_path.exists() assert json.loads(output_path.read_text(encoding="utf-8")) == profile + assert profile["chainId"] == 61127 def test_transact_records_fee_profile_observation(monkeypatch, tmp_path): diff --git a/tests/gltest_cli/config/test_config_integration.py b/tests/gltest_cli/config/test_config_integration.py index 1043c02..b5c87f4 100644 --- a/tests/gltest_cli/config/test_config_integration.py +++ b/tests/gltest_cli/config/test_config_integration.py @@ -239,6 +239,11 @@ def test_multiple_networks(): assert general_config.get_network_name() == "testnet_asimov_custom" rpc_url = general_config.get_rpc_url() assert rpc_url == "https://testnet.example.com" + chain = general_config.get_chain() + assert chain.id == 55554 + assert chain.rpc_urls["default"]["http"] == [ + "https://testnet.example.com" + ] """ ) diff --git a/tests/gltest_cli/config/test_fee_profile_plugin.py b/tests/gltest_cli/config/test_fee_profile_plugin.py new file mode 100644 index 0000000..6d2358d --- /dev/null +++ b/tests/gltest_cli/config/test_fee_profile_plugin.py @@ -0,0 +1,30 @@ +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock + +from gltest_cli.config import plugin + + +def test_session_profile_uses_the_selected_runtime_chain_id(monkeypatch, tmp_path): + output_path = tmp_path / "fees.json" + general_config = SimpleNamespace( + get_fee_profile_path=Mock(return_value=output_path), + get_network_name=Mock(return_value="preview"), + get_fee_profile_headroom=Mock(return_value=1.25), + get_chain=Mock(return_value=SimpleNamespace(id=61997)), + ) + collector = SimpleNamespace( + write=Mock(return_value={"methods": {"create_bet": {}}}), + has_observations=Mock(return_value=True), + ) + monkeypatch.setattr(plugin, "get_general_config", lambda: general_config) + monkeypatch.setattr(plugin, "get_fee_profile_collector", lambda: collector) + + plugin.pytest_sessionfinish(session=None, exitstatus=0) + + collector.write.assert_called_once_with( + path=Path(output_path), + network="preview", + headroom=1.25, + chain_id=61997, + ) diff --git a/tests/gltest_cli/config/test_general_config.py b/tests/gltest_cli/config/test_general_config.py index 2723f42..c6dd72a 100644 --- a/tests/gltest_cli/config/test_general_config.py +++ b/tests/gltest_cli/config/test_general_config.py @@ -8,6 +8,7 @@ NetworkConfigData, ) from gltest_cli.config.constants import DEFAULT_ARTIFACTS_DIR, DEFAULT_CONTRACTS_DIR +from genlayer_py.chains import studio_devnet, studionet def test_general_config_artifacts_methods(): @@ -31,6 +32,48 @@ def test_general_config_artifacts_methods(): assert general_config.plugin_config.artifacts_dir == Path("plugin_artifacts") +def test_studio_devnet_is_available_as_a_preconfigured_chain(): + config = GeneralConfig( + user_config=UserConfig( + networks={ + "studio_devnet": NetworkConfigData( + id=studio_devnet.id, + url=studio_devnet.rpc_urls["default"]["http"][0], + chain_type="studio_devnet", + ) + }, + default_network="studio_devnet", + ) + ) + + chain = config.get_chain() + assert chain.id == 61997 + assert chain.rpc_urls["default"]["http"] == [ + "https://studio-dev.genlayer.com/api" + ] + assert config.check_studio_based_rpc() + + +def test_custom_studio_network_applies_its_id_and_url_to_the_chain(): + config = GeneralConfig( + user_config=UserConfig( + networks={ + "preview": NetworkConfigData( + id=62001, + url="https://preview.example/api", + chain_type="studionet", + ) + }, + default_network="preview", + ) + ) + + chain = config.get_chain() + assert chain.id == 62001 + assert chain.rpc_urls["default"]["http"] == ["https://preview.example/api"] + assert studionet.id == 61999 + + def test_general_config_artifacts_default(): """Test GeneralConfig artifacts directory with default values.""" user_config = UserConfig( diff --git a/tests/gltest_cli/config/test_plugin.py b/tests/gltest_cli/config/test_plugin.py index 92b10c2..4366a0c 100644 --- a/tests/gltest_cli/config/test_plugin.py +++ b/tests/gltest_cli/config/test_plugin.py @@ -19,8 +19,8 @@ def test_help_message(pytester): " file)", " --leader-only Run contracts in leader-only mode", " --chain-type=CHAIN_TYPE", - " Chain type (possible values: localnet, studionet,", - " testnet_asimov, testnet_bradbury)", + " Chain type (possible values: localnet, studio_devnet,", + " studionet, testnet_asimov, testnet_bradbury)", " --fee-profile=FEE_PROFILE", " Path to write a JSON fee profile for observed deploys", " and writes", diff --git a/tests/gltest_cli/config/test_user.py b/tests/gltest_cli/config/test_user.py index 0a78197..f07f4d8 100644 --- a/tests/gltest_cli/config/test_user.py +++ b/tests/gltest_cli/config/test_user.py @@ -71,6 +71,11 @@ def test_get_default_user_config(): assert isinstance(network.from_account, str) assert network.from_account in network.accounts + studio_preview = config.networks["studio_devnet"] + assert studio_preview.id == 61997 + assert studio_preview.url == "https://studio-dev.genlayer.com/api" + assert studio_preview.chain_type == "studio_devnet" + # Check paths assert isinstance(config.paths, PathConfig) assert config.paths.contracts == DEFAULT_CONTRACTS_DIR diff --git a/uv.lock b/uv.lock index 2afb723..1003183 100644 --- a/uv.lock +++ b/uv.lock @@ -688,14 +688,14 @@ wheels = [ [[package]] name = "genlayer-py" -version = "0.19.0rc1" +version = "0.19.0rc2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "web3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/9c/5b5938ac9065e881be2746a6dc1aefb6a858c4109397ab08de33ddb4d050/genlayer_py-0.19.0rc1.tar.gz", hash = "sha256:7c295bbccf48afc14c84e25f874b94b7c2d1ea9df438f2efa16d1a49dd99f952", size = 90613, upload-time = "2026-09-03T00:05:08.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/17/1a82ff1db9d6b33ca8a73c8edb9c1786860462ee1fa16eb5261e50b1b7ac/genlayer_py-0.19.0rc2.tar.gz", hash = "sha256:92056aa1b9eb336550a86c7cc3dbd9c7b10afd18d9d846d094e4bc7dd8d7224a", size = 90588, upload-time = "2026-09-03T14:22:46.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/67eecbb71cf373f7128e822ece2d0de3ea4c976d31533163f18ab139dc34/genlayer_py-0.19.0rc1-py3-none-any.whl", hash = "sha256:3707c6741c98b71b7cec48f6133fa7264e373ea9a584bc2d69ad63a11f026efc", size = 106598, upload-time = "2026-09-03T00:05:06.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cb/b9df445c0aa09d0307561579b306e5a1133e736f1619b8bb1687ed8e8f50/genlayer_py-0.19.0rc2-py3-none-any.whl", hash = "sha256:40f96bb2b0ded2174a98533d199cbdfc57235255a5b8ed84a0b9464851cb708c", size = 106602, upload-time = "2026-09-03T14:22:45.109Z" }, ] [[package]] @@ -724,7 +724,7 @@ requires-dist = [ { name = "colorama", specifier = ">=0.4.6" }, { name = "eth-account", marker = "extra == 'sim'", specifier = ">=0.10" }, { name = "fastapi", marker = "extra == 'sim'", specifier = ">=0.100" }, - { name = "genlayer-py", specifier = ">=0.19.0rc1,<0.20.0" }, + { name = "genlayer-py", specifier = ">=0.19.0rc2,<0.20.0" }, { name = "httpx", marker = "extra == 'sim'", specifier = ">=0.24" }, { name = "numpy", marker = "extra == 'sim'", specifier = ">=1.26" }, { name = "pytest" },