"""Tests for the testbed bridge: tool behavior over the in-process transport.""" import asyncio import base64 import io import math 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, ) @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 async def test_animated_move_is_smooth_and_observable(bridge): """A move with duration glides: a concurrent client sees intermediate positions, and the final displacement is exact.""" # A transport is a single session: one per client. t1 = InProcessTransport.start(bridge) t2 = InProcessTransport.start(bridge) async with t1, t2: mover = AICCClient(t1) viewer = AICCClient(t2) async with mover, viewer: await mover.handshake() await viewer.handshake() start = (await mover.call_tool("proprioception", {})).output["position"] assert start["x"] == 1.5 and start["z"] == 1.5 move_task = asyncio.create_task( mover.call_tool("move", {"forward": 2.0, "duration": 0.8}) ) await asyncio.sleep(0.25) mid = (await viewer.call_tool("proprioception", {})).output["position"] # Moving along the 45-degree diagonal: strictly between start and end. assert 1.5 < mid["x"] < 3.5 and 1.5 < mid["z"] < 3.5 res = (await move_task).output assert res["moved"] == pytest.approx(2.0, abs=1e-3) assert res["position"]["x"] == pytest.approx( 1.5 + 2.0 * math.sin(math.radians(45)), abs=1e-3 ) assert res["duration"] == 0.8 async def test_animated_move_stops_on_collision(bridge): t = InProcessTransport.start(bridge) async with t: client = AICCClient(t) async with client: await client.handshake() bridge.world.capsule.yaw_deg = 180.0 # straight at the north wall res = ( await client.call_tool("move", {"forward": 5.0, "duration": 0.6}) ).output assert res["collision"] is True assert res["moved"] < 5.0 assert res["position"]["z"] == pytest.approx( bridge.world.capsule.radius, abs=1e-3 ) async def test_move_accepts_duration_zero_default(bridge): t = InProcessTransport.start(bridge) async with t: client = AICCClient(t) async with client: await client.handshake() res = (await client.call_tool("move", {"forward": 1.0})).output assert res["moved"] == pytest.approx(1.0) assert res["duration"] == 0.0