""" Tests for the Language Reasoning Module — CommandReasoner. Uses mocked OpenAI client to test interpretation logic without API calls. """ import json from unittest.mock import MagicMock, patch import pytest from src.language.reasoner import CommandReasoner class TestCommandReasoner: """Tests for CommandReasoner.interpret() method.""" @pytest.fixture def sample_detections(self) -> list[dict]: """Sample detection results for testing.""" 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_fallback_mode_without_api_key(self, sample_detections): """Reasoner without API key should use fallback mode.""" reasoner = CommandReasoner(provider="openai", api_key="", model="gpt-4o") result = reasoner.interpret( "Move the bottle to the box", sample_detections ) assert isinstance(result, dict) assert "action" in result assert "source" in result assert "destination" in result def test_fallback_extracts_action_verb(self, sample_detections): """Fallback mode should detect the action verb from the command.""" reasoner = CommandReasoner(provider="openai", api_key="", model="gpt-4o") result = reasoner.interpret( "inspect the cup carefully", sample_detections ) assert result["action"] == "inspect" def test_fallback_matches_labels(self, sample_detections): """Fallback mode should match detected labels from the command.""" reasoner = CommandReasoner(provider="openai", api_key="", model="gpt-4o") result = reasoner.interpret( "Move the bottle to the box", sample_detections ) assert result["source"] == "bottle" assert result["destination"] == "box" def test_empty_command_returns_error(self, sample_detections): """Empty command should return an error result.""" reasoner = CommandReasoner(provider="openai", api_key="", model="gpt-4o") result = reasoner.interpret("", sample_detections) assert result["action"] == "error" assert "error" in result @patch("src.language.reasoner.CommandReasoner._init_client") def test_llm_interpretation(self, mock_init, sample_detections): """Test successful LLM-based interpretation with mocked client.""" reasoner = CommandReasoner(provider="openai", api_key="test-key", model="gpt-4o") # Mock the OpenAI client response mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = json.dumps( {"action": "move", "source": "bottle", "destination": "box"} ) mock_client = MagicMock() mock_client.chat.completions.create.return_value = mock_response reasoner.client = mock_client result = reasoner.interpret( "Move the bottle to the box", sample_detections ) assert result["action"] == "move" assert result["source"] == "bottle" assert result["destination"] == "box" def test_parse_response_with_markdown_fences(self): """Parser should handle markdown code fences from LLM.""" reasoner = CommandReasoner(provider="openai", api_key="", model="gpt-4o") raw = '```json\n{"action": "pick_up", "source": "cup", "destination": null}\n```' result = reasoner._parse_response(raw) assert result["action"] == "pick_up" assert result["source"] == "cup" assert result["destination"] is None def test_parse_response_invalid_json(self): """Parser should return error dict for invalid JSON.""" reasoner = CommandReasoner(provider="openai", api_key="", model="gpt-4o") result = reasoner._parse_response("this is not json") assert result["action"] == "unknown" assert "error" in result