- conformance.py: declarative scenario runner (subset matcher, $kind/$gt/ $enum/$required assertions, call_id/session_id echo checks), reference bridge, CLI entry (python -m aicc.conformance) - bridge.py: protocol version check before parsing (protocol_mismatch), session expiry handling (session_expired), malformed tool_call -> invalid_input, emit_event() for async events - client.py: Self return type - in_process.py: drain bridge event queue after response (event delivery), Self return type - websocket.py: Self return type, best-effort shutdown - tests: 14 passing (incl. conformance scenarios, 9/9 core scenarios) - ruff: all checks pass
163 lines
5.0 KiB
Python
163 lines
5.0 KiB
Python
"""Integration test: end-to-end AICC session via in-process transport."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from aicc import AICCClient, Bridge, ToolClass
|
|
from aicc.protocol import ModelClass, TickMode, WorldKind
|
|
from aicc.transport.in_process import InProcessTransport
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handshake_and_call_tool():
|
|
bridge = Bridge(
|
|
name="test-room",
|
|
kind=WorldKind.TEXT,
|
|
tick_rate_hz=10.0,
|
|
tick_mode=TickMode.FIXED,
|
|
agent_model=ModelClass.EDGE_MEDIUM,
|
|
)
|
|
|
|
@bridge.tool(description="Get the agent's current position and rotation.")
|
|
async def proprioception() -> dict:
|
|
return {
|
|
"position": {"x": 1.0, "y": 0.5, "z": 2.0},
|
|
"rotation": {"yaw": 0.0, "pitch": 0.0},
|
|
"velocity": {"x": 0.0, "y": 0.0, "z": 0.0},
|
|
"health": 100,
|
|
}
|
|
|
|
@bridge.tool(description="Move the agent forward by the given distance.")
|
|
async def move(forward: float = 0.0) -> dict:
|
|
return {"moved": forward}
|
|
|
|
@bridge.tool(cls=ToolClass.GENERATOR, description="Place an object in the world.")
|
|
async def place_object(prefab: str) -> dict:
|
|
return {"spawned": prefab}
|
|
|
|
t = InProcessTransport.start(bridge)
|
|
async with t:
|
|
client = AICCClient(t)
|
|
async with client:
|
|
manifest = await client.handshake()
|
|
assert manifest.world.name == "test-room"
|
|
assert manifest.tick_rate_hz == 10.0
|
|
tool_ids = {t.id for t in manifest.tools}
|
|
assert {"proprioception", "move", "place_object"} <= tool_ids
|
|
|
|
res = await client.call_tool("proprioception", {})
|
|
assert res.ok
|
|
assert res.output["position"] == {"x": 1.0, "y": 0.5, "z": 2.0}
|
|
|
|
res = await client.call_tool("move", {"forward": 2.5})
|
|
assert res.ok
|
|
assert res.output == {"moved": 2.5}
|
|
|
|
res = await client.call_tool("place_object", {"prefab": "crate_01"})
|
|
assert res.ok
|
|
assert res.output == {"spawned": "crate_01"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_tool_returns_tool_error():
|
|
bridge = Bridge(name="x")
|
|
|
|
@bridge.tool(description="noop")
|
|
async def ping() -> dict:
|
|
return {"pong": True}
|
|
|
|
t = InProcessTransport.start(bridge)
|
|
async with t:
|
|
client = AICCClient(t)
|
|
async with client:
|
|
await client.handshake()
|
|
from aicc.errors import ToolError
|
|
from aicc.protocol import ErrorCode
|
|
|
|
with pytest.raises(ToolError) as ei:
|
|
await client.call_tool("does_not_exist", {})
|
|
assert ei.value.code == ErrorCode.TOOL_UNKNOWN
|
|
assert not ei.value.retryable
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_protocol_mismatch_raises():
|
|
from aicc.errors import ProtocolError
|
|
|
|
bridge = Bridge(name="x")
|
|
|
|
@bridge.tool(description="noop")
|
|
async def ping() -> dict:
|
|
return {"pong": True}
|
|
|
|
t = InProcessTransport.start(bridge)
|
|
async with t:
|
|
client = AICCClient(t, protocol="aicc/0.2")
|
|
async with client:
|
|
with pytest.raises(ProtocolError):
|
|
await client.handshake()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_capability_required_tool_is_unavailable():
|
|
from aicc.errors import ToolError
|
|
from aicc.protocol import ErrorCode
|
|
|
|
bridge = Bridge(name="x")
|
|
|
|
@bridge.tool(
|
|
description="place",
|
|
cls=ToolClass.GENERATOR,
|
|
requires_capability="can_modify_world",
|
|
)
|
|
async def place_object() -> dict:
|
|
return {"spawned": True}
|
|
|
|
t = InProcessTransport.start(bridge)
|
|
async with t:
|
|
client = AICCClient(t)
|
|
async with client:
|
|
await client.handshake()
|
|
with pytest.raises(ToolError) as ei:
|
|
await client.call_tool("place_object", {})
|
|
assert ei.value.code == ErrorCode.TOOL_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execution_failure_is_surfaced():
|
|
from aicc.errors import ToolError
|
|
from aicc.protocol import ErrorCode
|
|
|
|
bridge = Bridge(name="x")
|
|
|
|
@bridge.tool(description="always fails")
|
|
async def boom() -> dict:
|
|
raise RuntimeError("kaboom")
|
|
|
|
t = InProcessTransport.start(bridge)
|
|
async with t:
|
|
client = AICCClient(t)
|
|
async with client:
|
|
await client.handshake()
|
|
with pytest.raises(ToolError) as ei:
|
|
await client.call_tool("boom", {})
|
|
assert ei.value.code == ErrorCode.EXECUTION_FAILED
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_schema_generation_basic_types():
|
|
bridge = Bridge(name="x")
|
|
|
|
@bridge.tool(description="basic types")
|
|
async def fn(a: int, b: str, c: float = 1.0) -> dict:
|
|
return {"a": a, "b": b, "c": c}
|
|
|
|
schema = bridge.tools()[0].input_schema
|
|
assert schema["properties"]["a"]["type"] == "integer"
|
|
assert schema["properties"]["b"]["type"] == "string"
|
|
assert schema["properties"]["c"]["type"] == "number"
|
|
assert "a" in schema["required"]
|
|
assert "b" in schema["required"]
|
|
assert "c" not in schema["required"]
|