File size: 4,131 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
"""
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