File size: 4,313 Bytes
e1c010e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | """
Tests for the Action Planning Module — ActionPlanner.
"""
import pytest
from src.planner.action_planner import ActionPlanner
class TestActionPlanner:
"""Tests for ActionPlanner.plan() method."""
@pytest.fixture
def planner(self) -> ActionPlanner:
return ActionPlanner()
@pytest.fixture
def sample_detections(self) -> list[dict]:
return [
{"label": "bottle", "confidence": 0.92, "bbox": [100, 150, 200, 350]},
{"label": "box", "confidence": 0.85, "bbox": [400, 300, 550, 500]},
{"label": "cup", "confidence": 0.78, "bbox": [250, 200, 320, 300]},
]
def test_plan_returns_required_keys(self, planner, sample_detections):
"""Plan output must have all required keys."""
interpretation = {
"action": "move",
"source": "bottle",
"destination": "box",
}
result = planner.plan(sample_detections, interpretation)
assert "steps" in result
assert "source_bbox" in result
assert "dest_bbox" in result
assert "status" in result
assert "action" in result
def test_plan_success_status(self, planner, sample_detections):
"""Plan should succeed when both source and destination match."""
interpretation = {
"action": "move",
"source": "bottle",
"destination": "box",
}
result = planner.plan(sample_detections, interpretation)
assert result["status"] == "success"
assert result["source_bbox"] == [100, 150, 200, 350]
assert result["dest_bbox"] == [400, 300, 550, 500]
def test_plan_partial_status(self, planner, sample_detections):
"""Plan should be partial when destination doesn't match."""
interpretation = {
"action": "move",
"source": "bottle",
"destination": "nonexistent_object",
}
result = planner.plan(sample_detections, interpretation)
assert result["status"] == "partial"
assert result["source_bbox"] is not None
assert result["dest_bbox"] is None
def test_plan_failed_status(self, planner, sample_detections):
"""Plan should fail when source doesn't match."""
interpretation = {
"action": "move",
"source": "nonexistent_object",
"destination": "box",
}
result = planner.plan(sample_detections, interpretation)
assert result["status"] == "failed"
def test_plan_generates_steps(self, planner, sample_detections):
"""Plan should generate meaningful steps."""
interpretation = {
"action": "move",
"source": "bottle",
"destination": "box",
}
result = planner.plan(sample_detections, interpretation)
assert len(result["steps"]) >= 3
assert any("bottle" in step.lower() for step in result["steps"])
assert any("box" in step.lower() for step in result["steps"])
def test_plan_no_destination(self, planner, sample_detections):
"""Plan should work when destination is None (e.g. 'inspect')."""
interpretation = {
"action": "inspect",
"source": "cup",
"destination": None,
}
result = planner.plan(sample_detections, interpretation)
assert result["status"] == "success"
assert result["dest_bbox"] is None
def test_plan_empty_detections(self, planner):
"""Plan should fail gracefully with no detections."""
interpretation = {
"action": "move",
"source": "bottle",
"destination": "box",
}
result = planner.plan([], interpretation)
assert result["status"] == "failed"
def test_plan_with_synonym(self, planner, sample_detections):
"""Plan should resolve synonyms via ObjectMapper (mug → cup)."""
interpretation = {
"action": "pick_up",
"source": "mug",
"destination": None,
}
result = planner.plan(sample_detections, interpretation)
# "mug" should map to "cup" via the synonym mapper
assert result["status"] == "success"
assert result["source_bbox"] == [250, 200, 320, 300]
|