|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# ------------------------------------------------------------------------------ |
| 3 | +# |
| 4 | +# Copyright 2026 Valory AG |
| 5 | +# |
| 6 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +# you may not use this file except in compliance with the License. |
| 8 | +# You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, software |
| 13 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +# See the License for the specific language governing permissions and |
| 16 | +# limitations under the License. |
| 17 | +# |
| 18 | +# ------------------------------------------------------------------------------ |
| 19 | + |
| 20 | +"""Regression tests for the agent-level ``aea-config.yaml`` env-var wiring. |
| 21 | +
|
| 22 | +These guard against the class of bug introduced by renaming anonymous |
| 23 | +``${type:default}`` placeholders to named ``${NAME:type:default}`` ones. |
| 24 | +open-aea only applies the auto-derived component-path env var (e.g. |
| 25 | +``SKILL_..._ARGS_URL``) for *anonymous* placeholders; a *named* placeholder |
| 26 | +resolves solely against a bare env var of that exact name and otherwise |
| 27 | +silently falls back to its default. When the chosen name does not match what |
| 28 | +the runtime actually exports, the agent boots with the wrong (typically |
| 29 | +Omen/gnosis-flavored or ``0x``) default. |
| 30 | +
|
| 31 | +The agent-runner path resolves ``aea-config.yaml`` (not ``service.yaml``), so |
| 32 | +these mismatches only surface there. We resolve the real ``aea-config.yaml`` |
| 33 | +with open-aea's own resolver against a representative per-chain environment |
| 34 | +and assert the result is internally consistent for both deployment flavors. |
| 35 | +""" |
| 36 | + |
| 37 | +import io |
| 38 | +import json |
| 39 | +from pathlib import Path |
| 40 | +from typing import Any, Dict, List |
| 41 | +from unittest import mock |
| 42 | + |
| 43 | +import pytest |
| 44 | +from aea.helpers.env_vars import apply_env_variables_on_agent_config |
| 45 | +from aea.helpers.yaml_utils import yaml_load_all |
| 46 | + |
| 47 | +from packages.valory.skills.funds_manager.models import Params |
| 48 | + |
| 49 | +# packages/valory/skills/trader_abci/tests/ -> packages/valory/ |
| 50 | +_VALORY_DIR = Path(__file__).resolve().parents[3] |
| 51 | +AEA_CONFIG_PATH = _VALORY_DIR / "agents" / "trader" / "aea-config.yaml" |
| 52 | + |
| 53 | +# The path-derived env var the middleware exports for the (anonymous) mechs |
| 54 | +# marketplace subgraph url; open-aea derives this name from the config path. |
| 55 | +MECHS_SUBGRAPH_PATH_KEY = "SKILL_TRADER_ABCI_MODELS_MECHS_SUBGRAPH_ARGS_URL" |
| 56 | + |
| 57 | +SAFE = "0x318bF3775A8DE43ac803cfeDE45F62e256e3a7EC" |
| 58 | + |
| 59 | +# A minimal, valid per-chain fund requirement (chain key is added per test). |
| 60 | +_FUND_REQUIREMENT = { |
| 61 | + "agent": { |
| 62 | + "0x0000000000000000000000000000000000000000": { |
| 63 | + "topup": 100000000000000000, |
| 64 | + "threshold": 50000000000000000, |
| 65 | + } |
| 66 | + }, |
| 67 | + "safe": { |
| 68 | + "0x0000000000000000000000000000000000000000": { |
| 69 | + "topup": 5000000000000000000, |
| 70 | + "threshold": 2500000000000000000, |
| 71 | + } |
| 72 | + }, |
| 73 | +} |
| 74 | + |
| 75 | + |
| 76 | +def _resolve(env: Dict[str, str]) -> List[Dict[str, Any]]: |
| 77 | + """Resolve the real aea-config.yaml against ``env`` with open-aea's resolver. |
| 78 | +
|
| 79 | + :param env: the environment variables to apply during resolution. |
| 80 | + :return: the agent config documents with env vars substituted. |
| 81 | + """ |
| 82 | + docs = list(yaml_load_all(io.StringIO(AEA_CONFIG_PATH.read_text(encoding="utf-8")))) |
| 83 | + return apply_env_variables_on_agent_config(docs, env) |
| 84 | + |
| 85 | + |
| 86 | +def _override(docs: List[Dict[str, Any]], component: str) -> Dict[str, Any]: |
| 87 | + """Return the resolved override doc whose public id contains ``component``. |
| 88 | +
|
| 89 | + :param docs: the resolved agent config documents. |
| 90 | + :param component: substring to match against each override's public id. |
| 91 | + :return: the matching resolved override document. |
| 92 | + :raises AssertionError: if no override matches ``component``. |
| 93 | + """ |
| 94 | + for doc in docs: |
| 95 | + if component in str(doc.get("public_id", "")): |
| 96 | + return doc |
| 97 | + raise AssertionError(f"override for {component!r} not found in aea-config.yaml") |
| 98 | + |
| 99 | + |
| 100 | +@pytest.mark.parametrize( |
| 101 | + "chain, rpc_var, rpc_url", |
| 102 | + [ |
| 103 | + ("polygon", "POLYGON_LEDGER_RPC", "https://polygon.example/rpc"), |
| 104 | + ("gnosis", "GNOSIS_LEDGER_RPC", "https://gnosis.example/rpc"), |
| 105 | + ], |
| 106 | +) |
| 107 | +def test_agent_config_resolves_consistently_per_chain( |
| 108 | + chain: str, rpc_var: str, rpc_url: str |
| 109 | +) -> None: |
| 110 | + """The resolved agent config is chain-consistent for both flavors. |
| 111 | +
|
| 112 | + Reproduces the failure modes fixed for the polymarket Safe, |
| 113 | + ``funds_manager.rpc_urls`` and the mechs marketplace subgraph: each must |
| 114 | + resolve to the active chain rather than a stale gnosis/``0x`` default. |
| 115 | +
|
| 116 | + :param chain: the active deployment chain (e.g. ``polygon``/``gnosis``). |
| 117 | + :param rpc_var: the bare env var name carrying that chain's ledger RPC. |
| 118 | + :param rpc_url: the RPC url injected via ``rpc_var`` for the run. |
| 119 | + """ |
| 120 | + marketplace_url = ( |
| 121 | + f"https://api.subgraph.autonolas.tech/api/proxy/marketplace-{chain}" |
| 122 | + ) |
| 123 | + env = { |
| 124 | + "SAFE_CONTRACT_ADDRESSES": json.dumps({chain: SAFE}), |
| 125 | + rpc_var: rpc_url, |
| 126 | + MECHS_SUBGRAPH_PATH_KEY: marketplace_url, |
| 127 | + } |
| 128 | + docs = _resolve(env) |
| 129 | + |
| 130 | + # funds_manager: rpc_urls / safe must cover the active chain with real values. |
| 131 | + fm_args = _override(docs, "funds_manager")["models"]["params"]["args"] |
| 132 | + assert fm_args["rpc_urls"].get(chain) == rpc_url |
| 133 | + assert fm_args["safe_contract_addresses"].get(chain) == SAFE |
| 134 | + |
| 135 | + # The exact invariant that crashed the agent runner: fund_requirements' |
| 136 | + # chains must be a subset of rpc_urls and safe_contract_addresses. Drive |
| 137 | + # the real validation (Params.__init__ -> _validate_chain_keys). |
| 138 | + Params( |
| 139 | + name="params", |
| 140 | + skill_context=mock.MagicMock(skill_id="valory/funds_manager:0.1.0"), |
| 141 | + fund_requirements={chain: _FUND_REQUIREMENT}, |
| 142 | + rpc_urls=fm_args["rpc_urls"], |
| 143 | + safe_contract_addresses=fm_args["safe_contract_addresses"], |
| 144 | + ) |
| 145 | + |
| 146 | + # polymarket_client Safe must resolve to the provided address, never "0x". |
| 147 | + pm_safe = _override(docs, "polymarket_client")["config"]["safe_contract_addresses"] |
| 148 | + assert pm_safe.get(chain) == SAFE |
| 149 | + |
| 150 | + # mechs marketplace subgraph must follow the active chain (anonymous |
| 151 | + # placeholder -> path-key), not the gnosis default baked into the yaml. |
| 152 | + mechs_url = _override(docs, "trader_abci")["models"]["mechs_subgraph"]["args"][ |
| 153 | + "url" |
| 154 | + ] |
| 155 | + assert mechs_url == marketplace_url |
0 commit comments