""" 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]