Files
py/tests/test_app_rpc.py
zeekay e8ec8eed73 py: real zero-copy wire + RPC + PQ crypto; fix broken package
Make the published zap-proto package contain real code instead of stubs.

Wire (the product, was entirely absent):
- src/zap/wire.py — pure-stdlib port of the canonical Go runtime (zap.go +
  builder.py + accessors + list cursors). Message/Object/List zero-copy reads,
  Builder/ObjectBuilder/ListBuilder writes, 16-byte ZAP\x00 header, Version1+2
  accept, reader hardening (reject backward pointers into the header, clamp list
  length). Byte-faithful to Go — pinned by the shared cross-wire golden vector.

RPC (was a sleep-loop server + all-mock client):
- src/zap/rpc.py — real TCP request/response transport, Server dispatches the
  Zap interface method ordinals (@0..@8 from zap.zap), Client calls them.
- app.run() now serves real RPC; client.Client is a real RPC client.

Crypto (was secrets.token_bytes() fakes whose two sides never agreed and whose
verify() accepted any blob):
- src/zap/crypto.py — real ML-KEM-768 / ML-DSA-65 (pqcrypto) + X25519
  (cryptography), hybrid HKDF-SHA256 KEX. Hard-fails with CryptoError when a
  backend is missing — never fabricates bytes.

Package wiring:
- __init__.py exposes the wire API with ZERO third-party dep on the import path;
  pydantic/httpx are lazy (app.ZAP, Client). All __all__ names resolve on a
  default install (was 18 of 22 unresolved, swallowed ImportError).
- pyproject: drop nonexistent pyzap>=2.0.0; pydantic -> [app]; add [crypto];
  fix repo URL to zap-proto/py; target py312.

Bug fixes (audit PoCs):
- frame.py: reject oversize routing ids cleanly (was u16 struct crash); lenient
  decode of non-UTF8 ids (was strict-decode crash); encode/decode_cmd type tag
  so str round-trips to str (was silently bytes).
- protocol.py: decode_stream reports bytes consumed (decode dropped 2nd frame).
- app.py: _match_uri_template escapes literal template text (was regex over-match).
- consensus.py: fix votes int/dict type collision.

Quality: ruff clean, ruff format clean, mypy --strict 0 issues, 52 tests pass.
Add .github/workflows/ci.yml (ruff + format + mypy + pytest, py3.10-3.13).
Rebrand: zap.capnp -> zap.zap, $Go.import -> github.com/zap-proto/go.
2026-06-20 00:00:29 -07:00

63 lines
1.8 KiB
Python

"""End-to-end: a real ZAP app served over real RPC, driven by the real Client.
Proves the decorator-registered tools/resources/prompts are reachable across an
actual TCP socket through the method-ordinal dispatch — no mocks, no sleep loop.
"""
import threading
import pytest
pytest.importorskip("pydantic")
from zap import ZAP # noqa: E402 (after importorskip)
from zap.client import Client # noqa: E402
from zap.types import PromptMessage # noqa: E402
def _make_app() -> ZAP:
app = ZAP("e2e-agent", version="9.9.9")
@app.tool
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@app.resource("mem://{key}")
def read_mem(key: str) -> str:
return f"value-of-{key}"
@app.prompt
def greet(name: str) -> list[PromptMessage]:
return [PromptMessage(role="assistant", content=f"Hi {name}")]
return app
def test_app_served_over_rpc():
app = _make_app()
server = app.build_rpc_server(host="127.0.0.1", port=0)
server.bind()
t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
try:
with Client(f"127.0.0.1:{server.port}") as client:
info = client.connect()
assert info.name == "e2e-agent"
assert info.version == "9.9.9"
tools = client.list_tools()
assert [t.name for t in tools] == ["add"]
result = client.call_tool("add", {"a": 5, "b": 6})
assert result.content == b"11"
assert result.error == ""
content = client.read_resource("mem://k1")
assert content.text == "value-of-k1"
messages = client.get_prompt("greet", {"name": "Ada"})
assert messages[0].content == "Hi Ada"
finally:
server.close()