testbed: physics fixes (rest-at-contact), pytest suite (33 tests green)
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""Tests for the testbed bridge: tool behavior over the in-process transport."""
|
||||
|
||||
import base64
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from aicc.client import AICCClient
|
||||
from aicc.errors import ToolError
|
||||
from aicc.transport.in_process import InProcessTransport
|
||||
|
||||
from testbed.bridge import (
|
||||
DEPTH_HEIGHT,
|
||||
DEPTH_WIDTH,
|
||||
VISION_HEIGHT,
|
||||
VISION_WIDTH,
|
||||
build_bridge,
|
||||
)
|
||||
from testbed.room.world import Room
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def bridge():
|
||||
b = build_bridge()
|
||||
yield b
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(bridge):
|
||||
t = InProcessTransport.start(bridge)
|
||||
async with t:
|
||||
client = AICCClient(t)
|
||||
async with client:
|
||||
yield client
|
||||
|
||||
|
||||
async def test_manifest_lists_all_tools(client):
|
||||
m = await client.handshake()
|
||||
ids = [t.id for t in m.tools]
|
||||
for expected in (
|
||||
"proprioception",
|
||||
"vision",
|
||||
"depth",
|
||||
"hear",
|
||||
"world_query",
|
||||
"move",
|
||||
"turn",
|
||||
"look_at",
|
||||
"interact",
|
||||
"echo",
|
||||
"boom",
|
||||
"bump",
|
||||
):
|
||||
assert expected in ids
|
||||
assert "testbed_room_01" in m.world.name
|
||||
|
||||
|
||||
async def test_proprioception(client):
|
||||
await client.handshake()
|
||||
out = (await client.call_tool("proprioception", {})).output
|
||||
assert out["position"] == {"x": 1.5, "y": 0.0, "z": 1.5}
|
||||
assert out["rotation"]["yaw_deg"] == 45.0
|
||||
assert out["health"] == 100.0
|
||||
assert out["tick"] == 1
|
||||
|
||||
|
||||
async def test_vision_returns_png(client):
|
||||
await client.handshake()
|
||||
out = (await client.call_tool("vision", {})).output
|
||||
assert out["width"] == VISION_WIDTH
|
||||
assert out["height"] == VISION_HEIGHT
|
||||
raw = base64.b64decode(out["png_b64"])
|
||||
assert raw[:8] == b"\x89PNG\r\n\x1a\n"
|
||||
img = io.BytesIO(raw)
|
||||
from PIL import Image
|
||||
|
||||
assert Image.open(img).size == (VISION_WIDTH, VISION_HEIGHT)
|
||||
|
||||
|
||||
async def test_move_and_collision_event(client):
|
||||
await client.handshake()
|
||||
out = (await client.call_tool("move", {"forward": 2.0})).output
|
||||
assert out["moved"] == pytest.approx(2.0)
|
||||
assert out["collision"] is False
|
||||
out2 = (await client.call_tool("move", {"forward": 5.0})).output
|
||||
assert out2["collision"] is True
|
||||
assert out2["collision_normal"] is not None
|
||||
event = await client.next_event(timeout=1.0)
|
||||
assert event.topic == "collision"
|
||||
assert "other" in event.payload
|
||||
|
||||
|
||||
async def test_look_at_and_interact(client):
|
||||
await client.handshake()
|
||||
la = (await client.call_tool("look_at", {"target": "beacon"})).output
|
||||
assert abs(la["rotation"]["yaw_deg"] - 45.0) < 0.1
|
||||
res = (await client.call_tool("interact", {})).output
|
||||
assert res["success"] is False
|
||||
assert "too far" in res["message"]
|
||||
|
||||
|
||||
async def test_look_at_unknown_target(client):
|
||||
await client.handshake()
|
||||
with pytest.raises(ToolError) as ei:
|
||||
await client.call_tool("look_at", {"target": "moon"})
|
||||
assert ei.value.code == "execution_failed"
|
||||
|
||||
|
||||
async def test_hear_empty_then_hum(client, bridge):
|
||||
await client.handshake()
|
||||
out = (await client.call_tool("hear", {})).output
|
||||
assert out["sounds"] == []
|
||||
bridge.world.capsule.x = bridge.world.beacon.x - 3.0
|
||||
bridge.world.capsule.z = bridge.world.beacon.z
|
||||
out = (await client.call_tool("hear", {})).output
|
||||
assert any(s["kind"] == "beacon_hum" for s in out["sounds"])
|
||||
|
||||
|
||||
async def test_depth_and_world_query(client):
|
||||
await client.handshake()
|
||||
d = (await client.call_tool("depth", {})).output
|
||||
assert d["width"] == DEPTH_WIDTH and d["height"] == DEPTH_HEIGHT
|
||||
assert len(d["depth"]) == DEPTH_HEIGHT and len(d["depth"][0]) == DEPTH_WIDTH
|
||||
wq = (await client.call_tool("world_query", {})).output
|
||||
assert wq["beacon"]["id"] == "beacon"
|
||||
assert len(wq["obstacles"]) == 3
|
||||
|
||||
|
||||
async def test_unknown_tool(client):
|
||||
await client.handshake()
|
||||
with pytest.raises(ToolError) as ei:
|
||||
await client.call_tool("definitely_not_a_tool", {})
|
||||
assert ei.value.code == "tool_unknown"
|
||||
|
||||
|
||||
async def test_boom_execution_failed(client):
|
||||
await client.handshake()
|
||||
with pytest.raises(ToolError) as ei:
|
||||
await client.call_tool("boom", {})
|
||||
assert ei.value.code == "execution_failed"
|
||||
|
||||
|
||||
async def test_bump_emits_event(client):
|
||||
await client.handshake()
|
||||
out = (await client.call_tool("bump", {})).output
|
||||
assert out == {"bumped": True}
|
||||
event = await client.next_event(timeout=1.0)
|
||||
assert event.topic == "collision"
|
||||
|
||||
|
||||
async def test_echo(client):
|
||||
await client.handshake()
|
||||
out = (await client.call_tool("echo", {"value": "hello"})).output
|
||||
assert out == {"value": "hello"}
|
||||
|
||||
|
||||
async def test_interact_success_near_beacon(client, bridge):
|
||||
await client.handshake()
|
||||
bridge.world.capsule.x = bridge.world.beacon.x - 1.0
|
||||
bridge.world.capsule.z = bridge.world.beacon.z
|
||||
res = (await client.call_tool("interact", {})).output
|
||||
assert res["success"] is True
|
||||
assert bridge.world.beacon.active
|
||||
|
||||
|
||||
async def test_move_invalid_input(client):
|
||||
await client.handshake()
|
||||
with pytest.raises(ToolError) as ei:
|
||||
await client.call_tool("move", {"forward": 99.0})
|
||||
assert ei.value.code == "execution_failed"
|
||||
|
||||
|
||||
async def test_world_reset_between_sessions():
|
||||
"""Each fresh bridge owns a fresh Room."""
|
||||
b1, b2 = build_bridge(), build_bridge()
|
||||
assert b1.world is not b2.world
|
||||
|
||||
|
||||
async def test_single_source_of_truth():
|
||||
"""Manifest must carry no world state."""
|
||||
bridge = build_bridge()
|
||||
t = InProcessTransport.start(bridge)
|
||||
async with t:
|
||||
client = AICCClient(t)
|
||||
async with client:
|
||||
m = await client.handshake()
|
||||
dumped = m.model_dump(by_alias=True)
|
||||
world = dumped["world"]
|
||||
assert set(world.keys()) == {"name", "kind"}
|
||||
assert "beacon" not in dumped
|
||||
Reference in New Issue
Block a user