mirror of
https://github.com/hanzoai/agent.git
synced 2026-09-11 21:08:51 +00:00
* fix: declare what `import agents` needs, at floors 3.9 can install hanzo-agent 0.1.0 on PyPI cannot be imported from a clean install. On 3.9, agents/memory/store.py imports numpy at module level, and the wheel's METADATA does not declare it. On 3.10+, pip resolves openai 3.13.0, which replaced its httpx dependency with httpx2. agents imports httpx directly, in tracing/processors.py and models/openai_provider.py, and until now only got it through openai. main declares numpy>=2.1.0, but numpy 2.1.0 dropped 3.9, whose newest numpy is 2.0.2. requires-python still admits 3.9, so the pin cannot resolve there, and `uv lock --check` fails. - numpy: >=1.26 below 3.10, and >=2.1.0 from 3.10, which keeps the cp313t floor - httpx: >=0.23.0,<1, the range openai 2.x required - store.py imports numpy inside search_by_embedding, as retriever.py already does, so `import agents` does not need it - uv.lock: relocked. No pinned version moves; the lock was already stale for the aiohttp and cryptography floors. tests/test_packaging.py checks the declared numpy range on 3.9-3.13, that `import agents` works with numpy blocked, and the in-memory embedding search. A new CI job builds the wheel, installs it into a clean venv on 3.9 and 3.12, and imports agents and numpy. Co-Authored-By: Claude Opus 5 <[email protected]> * fix: two more places 3.9 cannot evaluate a PEP 604 union tests/test_zap.py annotates ZapAgentConfig.__init__ with `list | None` and has no `from __future__ import annotations`, so it fails collection on 3.9. That was the one error left in `pytest tests` there. agents/extensions/web3/mpc_client.py declares a pydantic field as `str | None`. Its future import does not help, because pydantic evaluates the annotation when it builds the model, and on 3.9 it can only do that with eval_type_backport installed. So `import agents.extensions.web3` fails on 3.9 even with [web3] installed. Declare the backport in the web3 extra, below 3.10, instead of rewriting the field as Optional[str]. The repo's ruff UP rules would flag Optional there, and their autofix would put the union back. On 3.9, `pytest tests` now gives 369 passed, and the web3 extension's own tests give 20 passed; before, they could not even be collected. 0.2.0, the declared floor, passes them too. Co-Authored-By: Claude Opus 5 <[email protected]> * fix: raise the web3 extra's floor to 6.20.2 On a clean 3.9 install of hanzo-agent[web3], the resolver picked web3 6.11.0 with eth-typing 5.2.1 and eth-account 0.13.7. web3 releases before 6.20.2 declare no upper bound on eth-typing, and web3 6 cannot import with eth-typing 5 ("cannot import name 'ContractName'"). 6.11 also imports pkg_resources, which a bare venv does not have. 6.20.2 is the first 6.x that caps eth-typing below 5, and 6.17.1 was the first to cap eth-account below 0.13. uv.lock already pins web3 6.20.4, so no pinned version moves. A clean [web3] install now resolves web3 6.20.4, eth-typing 4.4.0 and eth-account 0.11.3 on both 3.9 and 3.12, with no setuptools, and the extension's tests pass on both (20 each). Co-Authored-By: Claude Opus 5 <[email protected]> --------- Co-authored-by: Claude Opus 5 <[email protected]>
320 lines
9.9 KiB
Python
320 lines
9.9 KiB
Python
"""Tests for ZAP integration module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import os
|
|
|
|
# Add src to path to avoid going through agents.__init__ which has numpy dep
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
|
|
import pytest
|
|
|
|
# Import directly from zap submodule files
|
|
import importlib.util
|
|
|
|
|
|
def _load_module(name: str, path: str):
|
|
"""Load a module directly from file path."""
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
mod = importlib.util.module_from_spec(spec) # type: ignore
|
|
sys.modules[name] = mod
|
|
spec.loader.exec_module(mod) # type: ignore
|
|
return mod
|
|
|
|
|
|
# Load zap modules directly
|
|
_base = os.path.join(os.path.dirname(__file__), "..", "src", "agents", "zap")
|
|
_types = _load_module("agents.zap.types", os.path.join(_base, "types.py"))
|
|
_client = _load_module("agents.zap.client", os.path.join(_base, "client.py"))
|
|
|
|
# Import from loaded modules
|
|
Tool = _types.Tool
|
|
ToolId = _types.ToolId
|
|
ToolResult = _types.ToolResult
|
|
ZapError = _types.ZapError
|
|
ErrorCode = _types.ErrorCode
|
|
Effect = _types.Effect
|
|
Scope = _types.Scope
|
|
Stability = _types.Stability
|
|
CallContext = _types.CallContext
|
|
Hello = _types.Hello
|
|
Welcome = _types.Welcome
|
|
ConsensusConfig = _types.ConsensusConfig
|
|
ZapMessage = _types.ZapMessage
|
|
|
|
ZapClient = _client.ZapClient
|
|
ZapConnectionError = _client.ZapConnectionError
|
|
|
|
|
|
# For ZapAgentConfig, we need to mock the agent module dependencies
|
|
class ZapAgentConfig:
|
|
"""Mock config for testing."""
|
|
|
|
def __init__(
|
|
self,
|
|
gateway_uri: str = "zap://localhost:9999",
|
|
connect_timeout: float = 5.0,
|
|
request_timeout: float = 30.0,
|
|
tool_namespaces: list | None = None,
|
|
tool_prefixes: list | None = None,
|
|
include_canonical: bool = True,
|
|
certified_only: bool = False,
|
|
enable_consensus: bool = False,
|
|
consensus_participants: list | None = None,
|
|
):
|
|
self.gateway_uri = gateway_uri
|
|
self.connect_timeout = connect_timeout
|
|
self.request_timeout = request_timeout
|
|
self.tool_namespaces = tool_namespaces
|
|
self.tool_prefixes = tool_prefixes
|
|
self.include_canonical = include_canonical
|
|
self.certified_only = certified_only
|
|
self.enable_consensus = enable_consensus
|
|
self.consensus_participants = consensus_participants or []
|
|
|
|
|
|
class TestToolId:
|
|
"""Tests for ToolId parsing and formatting."""
|
|
|
|
def test_parse_full(self):
|
|
tid = ToolId.parse("native/[email protected]")
|
|
assert tid.namespace == "native"
|
|
assert tid.name == "fs.read"
|
|
assert tid.version == "1.0.0"
|
|
|
|
def test_parse_no_version(self):
|
|
tid = ToolId.parse("native/fs.read")
|
|
assert tid.namespace == "native"
|
|
assert tid.name == "fs.read"
|
|
assert tid.version == "1.0.0"
|
|
|
|
def test_parse_no_namespace(self):
|
|
tid = ToolId.parse("fs.read")
|
|
assert tid.namespace == "native"
|
|
assert tid.name == "fs.read"
|
|
|
|
def test_str(self):
|
|
tid = ToolId("mcp.github", "create_issue", "2.0.0")
|
|
assert str(tid) == "mcp.github/[email protected]"
|
|
|
|
def test_to_dict(self):
|
|
tid = ToolId("native", "test", "1.0.0")
|
|
d = tid.to_dict()
|
|
assert d == {"namespace": "native", "name": "test", "version": "1.0.0"}
|
|
|
|
|
|
class TestTool:
|
|
"""Tests for Tool type."""
|
|
|
|
def test_create(self):
|
|
tool = Tool(
|
|
id=ToolId("native", "test", "1.0.0"),
|
|
description="A test tool",
|
|
effect=Effect.PURE,
|
|
idempotent=True,
|
|
)
|
|
assert tool.name == "test"
|
|
assert tool.full_name == "native/[email protected]"
|
|
assert tool.effect == Effect.PURE
|
|
assert tool.idempotent is True
|
|
|
|
def test_serialization_roundtrip(self):
|
|
tool = Tool(
|
|
id=ToolId("native", "test", "1.0.0"),
|
|
description="A test tool",
|
|
effect=Effect.DETERMINISTIC,
|
|
input_schema={"type": "object", "properties": {"path": {"type": "string"}}},
|
|
)
|
|
d = tool.to_dict()
|
|
tool2 = Tool.from_dict(d)
|
|
assert tool2.name == tool.name
|
|
assert tool2.description == tool.description
|
|
assert tool2.effect == tool.effect
|
|
assert tool2.input_schema == tool.input_schema
|
|
|
|
|
|
class TestToolResult:
|
|
"""Tests for ToolResult type."""
|
|
|
|
def test_success(self):
|
|
result = ToolResult(success=True, data={"content": "hello"}, duration_ns=1000)
|
|
assert result.success is True
|
|
assert result.data == {"content": "hello"}
|
|
assert result.error is None
|
|
|
|
def test_error(self):
|
|
error = ZapError(ErrorCode.NOT_FOUND, "Tool not found")
|
|
result = ToolResult(success=False, error=error)
|
|
assert result.success is False
|
|
assert result.error.code == ErrorCode.NOT_FOUND
|
|
|
|
def test_serialization(self):
|
|
result = ToolResult(success=True, data={"x": 1}, duration_ns=500)
|
|
d = result.to_dict()
|
|
result2 = ToolResult.from_dict(d)
|
|
assert result2.success == result.success
|
|
assert result2.data == result.data
|
|
assert result2.duration_ns == result.duration_ns
|
|
|
|
|
|
class TestZapError:
|
|
"""Tests for ZapError type."""
|
|
|
|
def test_create(self):
|
|
err = ZapError(ErrorCode.INVALID_PARAMS, "Invalid path", details={"path": "/bad"})
|
|
assert err.code == ErrorCode.INVALID_PARAMS
|
|
assert err.message == "Invalid path"
|
|
assert err.details == {"path": "/bad"}
|
|
|
|
def test_serialization(self):
|
|
err = ZapError(ErrorCode.TIMEOUT, "Request timed out")
|
|
d = err.to_dict()
|
|
err2 = ZapError.from_dict(d)
|
|
assert err2.code == ErrorCode.TIMEOUT
|
|
assert err2.message == "Request timed out"
|
|
|
|
|
|
class TestZapMessage:
|
|
"""Tests for ZapMessage wire protocol."""
|
|
|
|
def test_encode_decode(self):
|
|
msg = ZapMessage(
|
|
type="catalog.listTools",
|
|
id="test-123",
|
|
payload={"certifiedOnly": False},
|
|
)
|
|
encoded = msg.encode()
|
|
# First 4 bytes are length
|
|
length = int.from_bytes(encoded[:4], "big")
|
|
assert length == len(encoded) - 4
|
|
|
|
# Decode
|
|
msg2 = ZapMessage.decode(encoded[4:])
|
|
assert msg2.type == msg.type
|
|
assert msg2.id == msg.id
|
|
assert msg2.payload == msg.payload
|
|
|
|
|
|
class TestZapClient:
|
|
"""Tests for ZapClient."""
|
|
|
|
def test_from_uri_tcp(self):
|
|
client = ZapClient.from_uri("zap://localhost:9999")
|
|
assert client.host == "localhost"
|
|
assert client.port == 9999
|
|
assert client.tls is False
|
|
assert client.unix_socket is None
|
|
|
|
def test_from_uri_tls(self):
|
|
client = ZapClient.from_uri("zap+tls://secure.example.com:8443")
|
|
assert client.host == "secure.example.com"
|
|
assert client.port == 8443
|
|
assert client.tls is True
|
|
|
|
def test_from_uri_unix(self):
|
|
client = ZapClient.from_uri("zap+unix:///var/run/zap.sock")
|
|
assert client.unix_socket == "/var/run/zap.sock"
|
|
|
|
def test_from_uri_default_port(self):
|
|
client = ZapClient.from_uri("zap://gateway.local")
|
|
assert client.host == "gateway.local"
|
|
assert client.port == 9999
|
|
|
|
def test_from_uri_invalid(self):
|
|
with pytest.raises(ValueError):
|
|
ZapClient.from_uri("http://example.com")
|
|
|
|
|
|
class TestZapAgentConfig:
|
|
"""Tests for ZapAgentConfig."""
|
|
|
|
def test_defaults(self):
|
|
config = ZapAgentConfig()
|
|
assert config.gateway_uri == "zap://localhost:9999"
|
|
assert config.connect_timeout == 5.0
|
|
assert config.include_canonical is True
|
|
assert config.certified_only is False
|
|
|
|
def test_custom(self):
|
|
config = ZapAgentConfig(
|
|
gateway_uri="zap+tls://prod.example.com:443",
|
|
tool_namespaces=["native", "mcp.github"],
|
|
certified_only=True,
|
|
)
|
|
assert config.gateway_uri == "zap+tls://prod.example.com:443"
|
|
assert config.tool_namespaces == ["native", "mcp.github"]
|
|
assert config.certified_only is True
|
|
|
|
|
|
class TestCallContext:
|
|
"""Tests for CallContext."""
|
|
|
|
def test_create(self):
|
|
ctx = CallContext(
|
|
trace_id="trace-abc",
|
|
span_id="span-123",
|
|
timeout_ms=5000,
|
|
)
|
|
assert ctx.trace_id == "trace-abc"
|
|
assert ctx.span_id == "span-123"
|
|
assert ctx.timeout_ms == 5000
|
|
|
|
def test_to_dict(self):
|
|
ctx = CallContext(trace_id="t1", span_id="s1", timeout_ms=3000)
|
|
d = ctx.to_dict()
|
|
assert d["traceId"] == "t1"
|
|
assert d["spanId"] == "s1"
|
|
assert d["timeout"] == 3000
|
|
|
|
|
|
class TestConsensusConfig:
|
|
"""Tests for ConsensusConfig."""
|
|
|
|
def test_defaults(self):
|
|
config = ConsensusConfig()
|
|
assert config.rounds == 3
|
|
assert config.k == 5
|
|
assert config.alpha == 0.6
|
|
assert config.beta1 == 0.8
|
|
assert config.beta2 == 0.9
|
|
|
|
def test_to_dict(self):
|
|
config = ConsensusConfig(rounds=5, k=7)
|
|
d = config.to_dict()
|
|
assert d["rounds"] == 5
|
|
assert d["k"] == 7
|
|
|
|
|
|
class TestHelloWelcome:
|
|
"""Tests for Hello/Welcome handshake."""
|
|
|
|
def test_hello(self):
|
|
hello = Hello()
|
|
d = hello.to_dict()
|
|
assert d["protocolVersion"] == "0.2.1"
|
|
assert d["clientInfo"]["name"] == "hanzo-agent"
|
|
assert d["capabilities"]["roots"] is True
|
|
|
|
def test_welcome_from_dict(self):
|
|
d = {
|
|
"protocolVersion": "0.2.1",
|
|
"endpointInfo": {"name": "test-gateway", "version": "1.0.0"},
|
|
"capabilities": {
|
|
"tools": True,
|
|
"resources": True,
|
|
"catalog": True,
|
|
"coordination": True,
|
|
},
|
|
"instructions": "Welcome to ZAP",
|
|
}
|
|
welcome = Welcome.from_dict(d)
|
|
assert welcome.endpoint_info.name == "test-gateway"
|
|
assert welcome.capabilities.tools is True
|
|
assert welcome.capabilities.catalog is True
|
|
assert welcome.instructions == "Welcome to ZAP"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|