Skip to content

Commit 3003f34

Browse files
Merge branch 'main' into test/acpClient
2 parents 88d5483 + d7ac87f commit 3003f34

15 files changed

Lines changed: 2665 additions & 103 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ __pycache__/
2121

2222
# Intellij IDEA files
2323
.idea/
24+
tests/.env
25+
htmlcov/
26+
.coverage

examples/acp_base/skip_evaluation/buyer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def on_new_task(job: ACPJob, memo_to_sign: Optional[ACPMemo] = None):
8787
"<your-schema-key-1>": "<your-schema-value-1>",
8888
"<your-schema-key-2>": "<your-schema-value-2>",
8989
},
90-
expired_at=datetime.now() + timedelta(minutes=3.1), # job expiry duration, minimum 3 minutes
90+
expired_at=datetime.now() + timedelta(minutes=5), # job expiry duration, minimum 3 minutes
9191
)
9292
logger.info(f"Job {job_id} initiated")
9393
logger.info("Listening for next steps...")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "virtuals-acp"
3-
version = "0.3.16"
3+
version = "0.3.18"
44
description = "Agent Commerce Protocol Python SDK by Virtuals"
55
authors = ["Steven Lee Soon Fatt <steven@virtuals.io>"]
66
readme = "README.md"

tests/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# General Variables
2+
WHITELISTED_WALLET_ADDRESS=<YOUR_WHITELISTED_WALLET_ADDRESS>
3+
WHITELISTED_WALLET_PRIVATE_KEY=<YOUR_WHITELISTED_PRIVATE_KEY>
4+
5+
# Seller Agent Variables
6+
SELLER_ENTITY_ID=<YOUR_SELLER_ENTITY_ID>
7+
SELLER_AGENT_WALLET_ADDRESS=<YOUR_SELLER_AGENT_WALLET_ADDRESS>
8+
9+
# Buyer Agent Variables
10+
BUYER_ENTITY_ID=<YOUR_BUYER_ENTITY_ID>
11+
BUYER_AGENT_WALLET_ADDRESS=<YOUR_BUYER_AGENT_WALLET_ADDRESS>

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55

6+
# Load .env file from tests directory if it exists
67
def pytest_configure(config):
78
"""Load environment variables from tests/.env before running tests"""
89
env_file = Path(__file__).parent / ".env"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
import os
3+
from virtuals_acp.contract_clients.contract_client_v2 import ACPContractClientV2
4+
5+
6+
# Skip all integration tests if environment variables are not set
7+
pytestmark = pytest.mark.integration
8+
9+
# Check if we have required environment variables for integration tests
10+
SKIP_INTEGRATION = not all([
11+
os.getenv("WHITELISTED_WALLET_PRIVATE_KEY"),
12+
os.getenv("SELLER_AGENT_WALLET_ADDRESS"),
13+
os.getenv("SELLER_ENTITY_ID"),
14+
])
15+
16+
17+
@pytest.mark.skipif(SKIP_INTEGRATION, reason="Integration test environment variables not set")
18+
class TestIntegrationACPContractClientV2:
19+
@pytest.fixture(scope="class")
20+
def integration_client(self):
21+
wallet_private_key = os.getenv("WHITELISTED_WALLET_PRIVATE_KEY")
22+
agent_wallet_address = os.getenv("SELLER_AGENT_WALLET_ADDRESS")
23+
entity_id = int(os.getenv("SELLER_ENTITY_ID", "0"))
24+
25+
try:
26+
client = ACPContractClientV2(
27+
agent_wallet_address=agent_wallet_address,
28+
wallet_private_key=wallet_private_key,
29+
entity_id=entity_id,
30+
)
31+
yield client
32+
except Exception as e:
33+
pytest.fail(f"Failed to initialize integration client: {e}")
34+
35+
class TestInitialization:
36+
def test_should_connect_to_mainnet(self, integration_client):
37+
assert integration_client is not None
38+
assert integration_client.agent_wallet_address is not None
39+
40+
def test_should_have_valid_web3_connection(self, integration_client):
41+
assert integration_client.w3 is not None
42+
assert integration_client.w3.is_connected()
43+
44+
def test_should_fetch_manager_addresses(self, integration_client):
45+
assert integration_client.job_manager_address is not None
46+
assert integration_client.job_manager_address.startswith("0x")
47+
48+
def test_should_validate_session_key(self, integration_client):
49+
# If client initialized, session key validation already passed
50+
assert integration_client.account is not None
51+
assert integration_client.entity_id is not None
52+
53+
def test_should_have_x402_instance(self, integration_client):
54+
assert integration_client.x402 is not None
55+
56+
def test_should_have_alchemy_kit_instance(self, integration_client):
57+
assert integration_client.alchemy_kit is not None

tests/unit/test_account.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import pytest
2+
import json
3+
from unittest.mock import MagicMock
4+
from virtuals_acp.account import ACPAccount
5+
from virtuals_acp.models import OperationPayload
6+
7+
TEST_AGENT_ADDRESS = "0x1234567890123456789012345678901234567890"
8+
TEST_PROVIDER_ADDRESS = "0x5555555555555555555555555555555555555555"
9+
10+
11+
class TestACPAccount:
12+
@pytest.fixture
13+
def mock_contract_client(self):
14+
"""Create a mock contract client"""
15+
return MagicMock()
16+
17+
@pytest.fixture
18+
def sample_metadata(self):
19+
"""Sample metadata for testing"""
20+
return {
21+
"service_name": "AI Trading Bot",
22+
"category": "trading",
23+
"tags": ["ai", "trading", "defi"]
24+
}
25+
26+
@pytest.fixture
27+
def account(self, mock_contract_client, sample_metadata):
28+
"""Create an ACPAccount instance for testing"""
29+
return ACPAccount(
30+
contract_client=mock_contract_client,
31+
id=123,
32+
client_address=TEST_AGENT_ADDRESS,
33+
provider_address=TEST_PROVIDER_ADDRESS,
34+
metadata=sample_metadata
35+
)
36+
37+
class TestInitialization:
38+
"""Test account initialization"""
39+
40+
def test_should_initialize_with_all_parameters(
41+
self, mock_contract_client, sample_metadata
42+
):
43+
"""Should correctly initialize account with all parameters"""
44+
account = ACPAccount(
45+
contract_client=mock_contract_client,
46+
id=456,
47+
client_address=TEST_AGENT_ADDRESS,
48+
provider_address=TEST_PROVIDER_ADDRESS,
49+
metadata=sample_metadata
50+
)
51+
52+
assert account.contract_client == mock_contract_client
53+
assert account.id == 456
54+
assert account.client_address == TEST_AGENT_ADDRESS
55+
assert account.provider_address == TEST_PROVIDER_ADDRESS
56+
assert account.metadata == sample_metadata
57+
58+
def test_should_initialize_with_empty_metadata(self, mock_contract_client):
59+
"""Should initialize with empty metadata dictionary"""
60+
account = ACPAccount(
61+
contract_client=mock_contract_client,
62+
id=789,
63+
client_address=TEST_AGENT_ADDRESS,
64+
provider_address=TEST_PROVIDER_ADDRESS,
65+
metadata={}
66+
)
67+
68+
assert account.metadata == {}
69+
70+
def test_should_store_metadata_reference(self, mock_contract_client):
71+
"""Should store reference to metadata dictionary"""
72+
metadata = {"key": "value"}
73+
account = ACPAccount(
74+
contract_client=mock_contract_client,
75+
id=111,
76+
client_address=TEST_AGENT_ADDRESS,
77+
provider_address=TEST_PROVIDER_ADDRESS,
78+
metadata=metadata
79+
)
80+
81+
assert account.metadata == {"key": "value"}
82+
83+
class TestUpdateMetadata:
84+
"""Test update_metadata method"""
85+
86+
def test_should_call_contract_client_with_json_string(
87+
self, account, mock_contract_client
88+
):
89+
"""Should call contract client's update_account_metadata with JSON string"""
90+
new_metadata = {"updated": "data", "version": 2}
91+
mock_operation = MagicMock(spec=OperationPayload)
92+
mock_contract_client.update_account_metadata.return_value = mock_operation
93+
94+
result = account.update_metadata(new_metadata)
95+
96+
# Verify contract client was called with correct parameters
97+
mock_contract_client.update_account_metadata.assert_called_once_with(
98+
123, # account.id
99+
json.dumps(new_metadata)
100+
)
101+
102+
# Verify operation payload was returned
103+
assert result == mock_operation
104+
105+
def test_should_serialize_complex_metadata_to_json(
106+
self, account, mock_contract_client
107+
):
108+
"""Should properly serialize complex metadata structures to JSON"""
109+
complex_metadata = {
110+
"name": "Test Service",
111+
"tags": ["tag1", "tag2"],
112+
"config": {
113+
"nested": {
114+
"value": 123,
115+
"enabled": True
116+
}
117+
}
118+
}
119+
mock_contract_client.update_account_metadata.return_value = MagicMock()
120+
121+
account.update_metadata(complex_metadata)
122+
123+
# Verify the JSON string is properly formatted
124+
call_args = mock_contract_client.update_account_metadata.call_args[0]
125+
assert call_args[0] == 123
126+
assert call_args[1] == json.dumps(complex_metadata)
127+
128+
# Verify JSON is valid by parsing it back
129+
parsed = json.loads(call_args[1])
130+
assert parsed == complex_metadata
131+
132+
def test_should_handle_empty_metadata_update(
133+
self, account, mock_contract_client
134+
):
135+
"""Should handle updating with empty metadata"""
136+
mock_contract_client.update_account_metadata.return_value = MagicMock()
137+
138+
account.update_metadata({})
139+
140+
mock_contract_client.update_account_metadata.assert_called_once_with(
141+
123,
142+
"{}"
143+
)
144+
145+
def test_should_update_different_account_ids(self, mock_contract_client):
146+
"""Should use correct account ID for different accounts"""
147+
account1 = ACPAccount(
148+
contract_client=mock_contract_client,
149+
id=111,
150+
client_address=TEST_AGENT_ADDRESS,
151+
provider_address=TEST_PROVIDER_ADDRESS,
152+
metadata={}
153+
)
154+
account2 = ACPAccount(
155+
contract_client=mock_contract_client,
156+
id=222,
157+
client_address=TEST_AGENT_ADDRESS,
158+
provider_address=TEST_PROVIDER_ADDRESS,
159+
metadata={}
160+
)
161+
162+
mock_contract_client.update_account_metadata.return_value = MagicMock()
163+
164+
account1.update_metadata({"account": 1})
165+
account2.update_metadata({"account": 2})
166+
167+
# Verify first call used account1's ID
168+
first_call = mock_contract_client.update_account_metadata.call_args_list[0]
169+
assert first_call[0][0] == 111
170+
171+
# Verify second call used account2's ID
172+
second_call = mock_contract_client.update_account_metadata.call_args_list[1]
173+
assert second_call[0][0] == 222
174+
175+
def test_should_not_modify_original_metadata_property(
176+
self, account, mock_contract_client
177+
):
178+
"""Should not modify the account's metadata property when updating"""
179+
original_metadata = account.metadata.copy()
180+
mock_contract_client.update_account_metadata.return_value = MagicMock()
181+
182+
new_metadata = {"completely": "different"}
183+
account.update_metadata(new_metadata)
184+
185+
# Original metadata should remain unchanged
186+
assert account.metadata == original_metadata
187+
188+
def test_should_return_operation_payload_from_contract_client(
189+
self, account, mock_contract_client
190+
):
191+
"""Should return the operation payload from contract client"""
192+
mock_operation = MagicMock(spec=OperationPayload)
193+
mock_operation.target = "0xContractAddress"
194+
mock_operation.data = "0xEncodedData"
195+
mock_operation.value = 0
196+
197+
mock_contract_client.update_account_metadata.return_value = mock_operation
198+
199+
result = account.update_metadata({"test": "data"})
200+
201+
assert result is mock_operation
202+
assert result.target == "0xContractAddress"
203+
assert result.data == "0xEncodedData"

0 commit comments

Comments
 (0)