recce-nightly 1.23.0.20251029__py3-none-any.whl → 1.23.0.20251031__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of recce-nightly might be problematic. Click here for more details.

@@ -0,0 +1,208 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+
5
+ # Skip all tests in this module if mcp is not available
6
+ pytest.importorskip("mcp")
7
+
8
+ from recce.core import RecceContext # noqa: E402
9
+ from recce.mcp_server import RecceMCPServer, run_mcp_server # noqa: E402
10
+ from recce.models.types import LineageDiff # noqa: E402
11
+ from recce.tasks.profile import ProfileDiffTask # noqa: E402
12
+ from recce.tasks.query import QueryDiffTask, QueryTask # noqa: E402
13
+ from recce.tasks.rowcount import RowCountDiffTask # noqa: E402
14
+
15
+
16
+ @pytest.fixture
17
+ def mcp_server():
18
+ """Fixture to create a RecceMCPServer instance for testing"""
19
+ mock_context = MagicMock(spec=RecceContext)
20
+ return RecceMCPServer(mock_context), mock_context
21
+
22
+
23
+ class TestRecceMCPServer:
24
+ """Test cases for the RecceMCPServer class"""
25
+
26
+ def test_server_initialization(self, mcp_server):
27
+ """Test that the MCP server initializes correctly"""
28
+ server, mock_context = mcp_server
29
+ assert server.context == mock_context
30
+ assert server.server is not None
31
+ assert server.server.name == "recce"
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_get_lineage_diff(self, mcp_server):
35
+ """Test the get_lineage_diff tool"""
36
+ server, mock_context = mcp_server
37
+ # Mock the lineage diff response
38
+ mock_lineage_diff = MagicMock(spec=LineageDiff)
39
+ mock_lineage_diff.model_dump.return_value = {
40
+ "added": ["model.project.new_model"],
41
+ "removed": ["model.project.old_model"],
42
+ "modified": ["model.project.changed_model"],
43
+ }
44
+ mock_context.get_lineage_diff.return_value = mock_lineage_diff
45
+
46
+ # Execute the method
47
+ result = await server._get_lineage_diff({})
48
+
49
+ # Verify the result
50
+ assert "added" in result
51
+ assert "removed" in result
52
+ assert "modified" in result
53
+ mock_context.get_lineage_diff.assert_called_once()
54
+ mock_lineage_diff.model_dump.assert_called_once_with(mode="json")
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_row_count_diff(self, mcp_server):
58
+ """Test the row_count_diff tool"""
59
+ server, _ = mcp_server
60
+ # Mock the task execution
61
+ mock_result = {"results": [{"node_id": "model.project.my_model", "base": 100, "current": 105, "diff": 5}]}
62
+
63
+ with patch.object(RowCountDiffTask, "execute", return_value=mock_result):
64
+ result = await server._row_count_diff({"node_names": ["my_model"]})
65
+
66
+ # Verify the result
67
+ assert result == mock_result
68
+ assert "results" in result
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_query(self, mcp_server):
72
+ """Test the query tool"""
73
+ server, _ = mcp_server
74
+ # Mock the task execution
75
+ mock_result = MagicMock()
76
+ mock_result.model_dump.return_value = {
77
+ "columns": ["id", "name"],
78
+ "data": [[1, "Alice"], [2, "Bob"]],
79
+ }
80
+
81
+ with patch.object(QueryTask, "execute", return_value=mock_result):
82
+ result = await server._query({"sql_template": "SELECT * FROM {{ ref('my_model') }}", "base": False})
83
+
84
+ # Verify the result
85
+ assert "columns" in result
86
+ assert "data" in result
87
+ mock_result.model_dump.assert_called_once_with(mode="json")
88
+
89
+ @pytest.mark.asyncio
90
+ async def test_query_with_base_flag(self, mcp_server):
91
+ """Test the query tool with base environment flag"""
92
+ server, _ = mcp_server
93
+ mock_result = {"columns": ["id"], "data": [[1]]}
94
+
95
+ with patch.object(QueryTask, "execute", return_value=mock_result) as mock_execute:
96
+ with patch.object(QueryTask, "__init__", return_value=None):
97
+ task = QueryTask(params={"sql_template": "SELECT 1"})
98
+ task.is_base = True
99
+ task.execute = mock_execute
100
+
101
+ result = await server._query({"sql_template": "SELECT 1", "base": True})
102
+
103
+ # Verify base flag was set (would need to inspect task creation)
104
+ assert result == mock_result
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_query_diff(self, mcp_server):
108
+ """Test the query_diff tool"""
109
+ server, _ = mcp_server
110
+ # Mock the task execution
111
+ mock_result = MagicMock()
112
+ mock_result.model_dump.return_value = {
113
+ "diff": {
114
+ "added": [[3, "Charlie"]],
115
+ "removed": [[1, "Alice"]],
116
+ "modified": [],
117
+ }
118
+ }
119
+
120
+ with patch.object(QueryDiffTask, "execute", return_value=mock_result):
121
+ result = await server._query_diff(
122
+ {
123
+ "sql_template": "SELECT * FROM {{ ref('my_model') }}",
124
+ "primary_keys": ["id"],
125
+ }
126
+ )
127
+
128
+ # Verify the result
129
+ assert "diff" in result
130
+ mock_result.model_dump.assert_called_once_with(mode="json")
131
+
132
+ @pytest.mark.asyncio
133
+ async def test_profile_diff(self, mcp_server):
134
+ """Test the profile_diff tool"""
135
+ server, _ = mcp_server
136
+ # Mock the task execution
137
+ mock_result = MagicMock()
138
+ mock_result.model_dump.return_value = {
139
+ "columns": {
140
+ "id": {
141
+ "base": {"min": 1, "max": 100, "avg": 50.5},
142
+ "current": {"min": 1, "max": 105, "avg": 53.0},
143
+ }
144
+ }
145
+ }
146
+
147
+ with patch.object(ProfileDiffTask, "execute", return_value=mock_result):
148
+ result = await server._profile_diff({"model": "my_model", "columns": ["id"]})
149
+
150
+ # Verify the result
151
+ assert "columns" in result
152
+ mock_result.model_dump.assert_called_once_with(mode="json")
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_error_handling(self, mcp_server):
156
+ """Test error handling in tool execution"""
157
+ server, mock_context = mcp_server
158
+ # Make get_lineage_diff raise an exception
159
+ mock_context.get_lineage_diff.side_effect = Exception("Test error")
160
+
161
+ # The method should raise the exception
162
+ with pytest.raises(Exception, match="Test error"):
163
+ await server._get_lineage_diff({})
164
+
165
+
166
+ class TestRunMCPServer:
167
+ """Test cases for the run_mcp_server function"""
168
+
169
+ @pytest.mark.asyncio
170
+ @patch("recce.mcp_server.load_context")
171
+ @patch.object(RecceMCPServer, "run")
172
+ async def test_run_mcp_server(self, mock_run, mock_load_context):
173
+ """Test the run_mcp_server entry point"""
174
+ # Mock the context
175
+ mock_context = MagicMock(spec=RecceContext)
176
+ mock_load_context.return_value = mock_context
177
+
178
+ # Mock the server run method
179
+ mock_run.return_value = None
180
+
181
+ # Run the server
182
+ await run_mcp_server(project_dir="/test/path")
183
+
184
+ # Verify context was loaded with correct kwargs
185
+ mock_load_context.assert_called_once_with(project_dir="/test/path")
186
+
187
+ # Verify server was run
188
+ mock_run.assert_called_once()
189
+
190
+ @pytest.mark.asyncio
191
+ @patch("recce.mcp_server.load_context")
192
+ async def test_run_mcp_server_context_error(self, mock_load_context):
193
+ """Test run_mcp_server handles context loading errors"""
194
+ # Make load_context raise an exception
195
+ mock_load_context.side_effect = FileNotFoundError("manifest.json not found")
196
+
197
+ # The function should raise the exception
198
+ with pytest.raises(FileNotFoundError):
199
+ await run_mcp_server()
200
+
201
+
202
+ def test_mcp_cli_command_exists():
203
+ """Test that the mcp-server CLI command is registered"""
204
+ from recce.cli import cli
205
+
206
+ # Check that mcp_server is in the CLI commands
207
+ commands = [cmd.name for cmd in cli.commands.values()]
208
+ assert "mcp_server" in commands or "mcp-server" in commands