lean-lsp-mcp 0.13.0__tar.gz → 0.13.2__tar.gz
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.
- {lean_lsp_mcp-0.13.0/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.13.2}/PKG-INFO +3 -3
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/pyproject.toml +3 -3
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/client_utils.py +8 -7
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/instructions.py +1 -1
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/server.py +3 -2
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2/src/lean_lsp_mcp.egg-info}/PKG-INFO +3 -3
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp.egg-info/requires.txt +2 -2
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/tests/test_diagnostic_line_range.py +65 -125
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/tests/test_file_caching.py +18 -24
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/LICENSE +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/README.md +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/setup.cfg +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/__init__.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/file_utils.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/search_utils.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp/utils.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/tests/test_logging.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/tests/test_project_tools.py +0 -0
- {lean_lsp_mcp-0.13.0 → lean_lsp_mcp-0.13.2}/tests/test_search_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,8 +8,8 @@ Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.5.
|
|
12
|
-
Requires-Dist: mcp[cli]==1.21.
|
|
11
|
+
Requires-Dist: leanclient==0.5.5
|
|
12
|
+
Requires-Dist: mcp[cli]==1.21.2
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Provides-Extra: lint
|
|
15
15
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lean-lsp-mcp"
|
|
3
|
-
version = "0.13.
|
|
3
|
+
version = "0.13.2"
|
|
4
4
|
description = "Lean Theorem Prover MCP"
|
|
5
5
|
authors = [{name="Oliver Dressler", email="hey@oli.show"}]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.10"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"leanclient==0.5.
|
|
11
|
-
"mcp[cli]==1.21.
|
|
10
|
+
"leanclient==0.5.5",
|
|
11
|
+
"mcp[cli]==1.21.2",
|
|
12
12
|
"orjson>=3.11.1",
|
|
13
13
|
]
|
|
14
14
|
|
|
@@ -36,14 +36,15 @@ def startup_client(ctx: Context):
|
|
|
36
36
|
client.close()
|
|
37
37
|
|
|
38
38
|
# Need to create a new client
|
|
39
|
+
# In test environments, prevent repeated cache downloads
|
|
40
|
+
prevent_cache = bool(os.environ.get("LEAN_LSP_TEST_MODE"))
|
|
39
41
|
with OutputCapture() as output:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
logger.info(f"Connected with initial build to {lean_project_path}")
|
|
42
|
+
client = LeanLSPClient(
|
|
43
|
+
lean_project_path,
|
|
44
|
+
initial_build=False,
|
|
45
|
+
prevent_cache_get=prevent_cache
|
|
46
|
+
)
|
|
47
|
+
logger.info(f"Connected to Lean language server at {lean_project_path}")
|
|
47
48
|
build_output = output.get_output()
|
|
48
49
|
if build_output:
|
|
49
50
|
logger.debug(f"Build output: {build_output}")
|
|
@@ -7,7 +7,7 @@ INSTRUCTIONS = """## General Rules
|
|
|
7
7
|
## Key Tools
|
|
8
8
|
- lean_local_search: Confirm declarations (theorems/lemmas/defs/etc.) exist. VERY USEFUL AND FAST!
|
|
9
9
|
- lean_goal: Check proof state. USE OFTEN!
|
|
10
|
-
- lean_diagnostic_messages: Understand current proof situation
|
|
10
|
+
- lean_diagnostic_messages: Understand current proof situation.
|
|
11
11
|
- lean_hover_info: Documentation about terms and lean syntax.
|
|
12
12
|
- lean_leansearch: Search theorems using natural language or Lean terms.
|
|
13
13
|
- lean_loogle: Search definitions and theorems by name, type, or subexpression.
|
|
@@ -289,6 +289,7 @@ def diagnostic_messages(
|
|
|
289
289
|
declaration_name (str, optional): Name of a specific theorem/lemma/definition.
|
|
290
290
|
If provided, only returns diagnostics within that declaration.
|
|
291
291
|
Takes precedence over start_line/end_line.
|
|
292
|
+
Slow, requires waiting for full file analysis.
|
|
292
293
|
|
|
293
294
|
Returns:
|
|
294
295
|
List[str] | str: Diagnostic msgs or error msg
|
|
@@ -314,7 +315,7 @@ def diagnostic_messages(
|
|
|
314
315
|
rel_path,
|
|
315
316
|
start_line=start_line_0,
|
|
316
317
|
end_line=end_line_0,
|
|
317
|
-
inactivity_timeout=
|
|
318
|
+
inactivity_timeout=15.0,
|
|
318
319
|
)
|
|
319
320
|
|
|
320
321
|
return format_diagnostics(diagnostics)
|
|
@@ -672,7 +673,7 @@ def run_code(ctx: Context, code: str) -> List[str] | str:
|
|
|
672
673
|
client.open_file(rel_path)
|
|
673
674
|
opened_file = True
|
|
674
675
|
diagnostics = format_diagnostics(
|
|
675
|
-
client.get_diagnostics(rel_path, inactivity_timeout=
|
|
676
|
+
client.get_diagnostics(rel_path, inactivity_timeout=15.0)
|
|
676
677
|
)
|
|
677
678
|
finally:
|
|
678
679
|
if opened_file:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,8 +8,8 @@ Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.5.
|
|
12
|
-
Requires-Dist: mcp[cli]==1.21.
|
|
11
|
+
Requires-Dist: leanclient==0.5.5
|
|
12
|
+
Requires-Dist: mcp[cli]==1.21.2
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Provides-Extra: lint
|
|
15
15
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
@@ -10,7 +10,7 @@ import pytest
|
|
|
10
10
|
from tests.helpers.mcp_client import MCPClient, result_text
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@pytest.fixture()
|
|
13
|
+
@pytest.fixture(scope="module")
|
|
14
14
|
def diagnostic_file(test_project_path: Path) -> Path:
|
|
15
15
|
path = test_project_path / "DiagnosticTest.lean"
|
|
16
16
|
content = textwrap.dedent(
|
|
@@ -34,46 +34,30 @@ def diagnostic_file(test_project_path: Path) -> Path:
|
|
|
34
34
|
trivial
|
|
35
35
|
"""
|
|
36
36
|
).strip()
|
|
37
|
-
path.
|
|
37
|
+
if not path.exists() or path.read_text(encoding="utf-8") != content + "\n":
|
|
38
|
+
path.write_text(content + "\n", encoding="utf-8")
|
|
38
39
|
return path
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
@pytest.mark.asyncio
|
|
42
|
-
async def
|
|
43
|
+
async def test_diagnostic_messages_line_filtering(
|
|
43
44
|
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
44
45
|
diagnostic_file: Path,
|
|
45
46
|
) -> None:
|
|
46
|
-
"""Test
|
|
47
|
+
"""Test all line range filtering scenarios in one client session."""
|
|
47
48
|
async with mcp_client_factory() as client:
|
|
49
|
+
# Test 1: Get all diagnostic messages without line range filtering
|
|
48
50
|
diagnostics = await client.call_tool(
|
|
49
51
|
"lean_diagnostic_messages",
|
|
50
52
|
{"file_path": str(diagnostic_file)},
|
|
51
53
|
)
|
|
52
54
|
diag_text = result_text(diagnostics)
|
|
53
|
-
|
|
54
55
|
# Should contain both errors
|
|
55
56
|
assert "string" in diag_text.lower() or "error" in diag_text.lower()
|
|
56
|
-
# Check that multiple diagnostics are returned (at least the two errors we created)
|
|
57
57
|
assert diag_text.count("severity") >= 2
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
async def test_diagnostic_messages_with_start_line(
|
|
62
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
63
|
-
diagnostic_file: Path,
|
|
64
|
-
) -> None:
|
|
65
|
-
"""Test getting diagnostic messages starting from a specific line."""
|
|
66
|
-
async with mcp_client_factory() as client:
|
|
67
|
-
# First get all diagnostics to see what we have
|
|
68
|
-
all_diagnostics = await client.call_tool(
|
|
69
|
-
"lean_diagnostic_messages",
|
|
70
|
-
{
|
|
71
|
-
"file_path": str(diagnostic_file),
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
all_diag_text = result_text(all_diagnostics)
|
|
75
|
-
|
|
76
|
-
# Get diagnostics starting from line 10 (should only include the second error)
|
|
58
|
+
all_diag_text = diag_text
|
|
59
|
+
|
|
60
|
+
# Test 2: Get diagnostics starting from line 10
|
|
77
61
|
diagnostics = await client.call_tool(
|
|
78
62
|
"lean_diagnostic_messages",
|
|
79
63
|
{
|
|
@@ -82,35 +66,15 @@ async def test_diagnostic_messages_with_start_line(
|
|
|
82
66
|
},
|
|
83
67
|
)
|
|
84
68
|
diag_text = result_text(diagnostics)
|
|
85
|
-
|
|
86
69
|
# Should contain the second error (line 13: anotherError)
|
|
87
70
|
assert "123" in diag_text or "error" in diag_text.lower()
|
|
88
|
-
# Should have fewer diagnostics than all_diagnostics
|
|
89
71
|
assert len(diag_text) < len(all_diag_text)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@pytest.mark.asyncio
|
|
93
|
-
async def test_diagnostic_messages_with_line_range(
|
|
94
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
95
|
-
diagnostic_file: Path,
|
|
96
|
-
) -> None:
|
|
97
|
-
"""Test getting diagnostic messages for a specific line range."""
|
|
98
|
-
async with mcp_client_factory() as client:
|
|
99
|
-
# First, get all diagnostics to see what lines they're actually on
|
|
100
|
-
all_diagnostics = await client.call_tool(
|
|
101
|
-
"lean_diagnostic_messages",
|
|
102
|
-
{"file_path": str(diagnostic_file)},
|
|
103
|
-
)
|
|
104
|
-
all_diag_text = result_text(all_diagnostics)
|
|
105
|
-
|
|
106
|
-
# Extract line numbers from the diagnostics (format: "l7c23-l7c31")
|
|
72
|
+
|
|
73
|
+
# Test 3: Get diagnostics for specific line range
|
|
107
74
|
import re
|
|
108
|
-
|
|
109
75
|
line_matches = re.findall(r"l(\d+)c", all_diag_text)
|
|
110
76
|
if line_matches:
|
|
111
77
|
first_error_line = int(line_matches[0])
|
|
112
|
-
|
|
113
|
-
# Get diagnostics only up to that error line
|
|
114
78
|
diagnostics = await client.call_tool(
|
|
115
79
|
"lean_diagnostic_messages",
|
|
116
80
|
{
|
|
@@ -120,21 +84,10 @@ async def test_diagnostic_messages_with_line_range(
|
|
|
120
84
|
},
|
|
121
85
|
)
|
|
122
86
|
diag_text = result_text(diagnostics)
|
|
123
|
-
|
|
124
|
-
# Should contain the first error
|
|
125
87
|
assert "string" in diag_text.lower() or len(diag_text) > 0
|
|
126
|
-
# Should be fewer diagnostics than all
|
|
127
88
|
assert len(diag_text) < len(all_diag_text)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
@pytest.mark.asyncio
|
|
131
|
-
async def test_diagnostic_messages_with_no_errors_in_range(
|
|
132
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
133
|
-
diagnostic_file: Path,
|
|
134
|
-
) -> None:
|
|
135
|
-
"""Test getting diagnostic messages for a range with no errors."""
|
|
136
|
-
async with mcp_client_factory() as client:
|
|
137
|
-
# Get diagnostics only for lines 14-17 (valid theorem, should have no errors)
|
|
89
|
+
|
|
90
|
+
# Test 4: Get diagnostics for range with no errors (lines 14-17)
|
|
138
91
|
diagnostics = await client.call_tool(
|
|
139
92
|
"lean_diagnostic_messages",
|
|
140
93
|
{
|
|
@@ -144,9 +97,6 @@ async def test_diagnostic_messages_with_no_errors_in_range(
|
|
|
144
97
|
},
|
|
145
98
|
)
|
|
146
99
|
diag_text = result_text(diagnostics)
|
|
147
|
-
|
|
148
|
-
# Should indicate no errors or be empty
|
|
149
|
-
# The exact format depends on how the tool formats an empty result
|
|
150
100
|
assert (
|
|
151
101
|
"no" in diag_text.lower()
|
|
152
102
|
or len(diag_text.strip()) == 0
|
|
@@ -154,7 +104,7 @@ async def test_diagnostic_messages_with_no_errors_in_range(
|
|
|
154
104
|
)
|
|
155
105
|
|
|
156
106
|
|
|
157
|
-
@pytest.fixture()
|
|
107
|
+
@pytest.fixture(scope="module")
|
|
158
108
|
def declaration_diagnostic_file(test_project_path: Path) -> Path:
|
|
159
109
|
"""Create a test file with multiple declarations, some with errors."""
|
|
160
110
|
path = test_project_path / "DeclarationDiagnosticTest.lean"
|
|
@@ -175,29 +125,28 @@ def declaration_diagnostic_file(test_project_path: Path) -> Path:
|
|
|
175
125
|
def anotherValidFunction : String := "hello"
|
|
176
126
|
"""
|
|
177
127
|
).strip()
|
|
178
|
-
path.
|
|
128
|
+
if not path.exists() or path.read_text(encoding="utf-8") != content + "\n":
|
|
129
|
+
path.write_text(content + "\n", encoding="utf-8")
|
|
179
130
|
return path
|
|
180
131
|
|
|
181
132
|
|
|
182
133
|
@pytest.mark.asyncio
|
|
183
|
-
async def
|
|
134
|
+
async def test_diagnostic_messages_declaration_filtering(
|
|
184
135
|
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
185
136
|
declaration_diagnostic_file: Path,
|
|
186
137
|
) -> None:
|
|
187
|
-
"""Test filtering
|
|
138
|
+
"""Test all declaration-based filtering scenarios in one client session."""
|
|
188
139
|
async with mcp_client_factory() as client:
|
|
189
|
-
# Get all diagnostics first to verify file has errors
|
|
140
|
+
# Test 1: Get all diagnostics first to verify file has errors
|
|
190
141
|
all_diagnostics = await client.call_tool(
|
|
191
142
|
"lean_diagnostic_messages",
|
|
192
143
|
{"file_path": str(declaration_diagnostic_file)},
|
|
193
144
|
)
|
|
194
145
|
all_diag_text = result_text(all_diagnostics)
|
|
195
|
-
|
|
196
|
-
# File should have diagnostics (contains intentional errors)
|
|
197
146
|
assert len(all_diag_text) > 0
|
|
198
147
|
assert "string" in all_diag_text.lower() or "type" in all_diag_text.lower()
|
|
199
148
|
|
|
200
|
-
# Get diagnostics for firstTheorem only
|
|
149
|
+
# Test 2: Get diagnostics for firstTheorem only
|
|
201
150
|
diagnostics = await client.call_tool(
|
|
202
151
|
"lean_diagnostic_messages",
|
|
203
152
|
{
|
|
@@ -206,33 +155,10 @@ async def test_diagnostic_messages_with_declaration_name_valid(
|
|
|
206
155
|
},
|
|
207
156
|
)
|
|
208
157
|
diag_text = result_text(diagnostics)
|
|
209
|
-
|
|
210
|
-
# Should contain error from firstTheorem
|
|
211
|
-
# The exact error message may vary, but should reference the theorem
|
|
212
158
|
assert len(diag_text) > 0
|
|
213
|
-
|
|
214
|
-
# Filtered diagnostics should be shorter than or equal to all diagnostics
|
|
215
159
|
assert len(diag_text) <= len(all_diag_text)
|
|
216
160
|
|
|
217
|
-
|
|
218
|
-
@pytest.mark.asyncio
|
|
219
|
-
async def test_diagnostic_messages_with_declaration_name_with_errors(
|
|
220
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
221
|
-
declaration_diagnostic_file: Path,
|
|
222
|
-
) -> None:
|
|
223
|
-
"""Test filtering by declaration that has type errors."""
|
|
224
|
-
async with mcp_client_factory() as client:
|
|
225
|
-
# Get all diagnostics first to verify file has errors
|
|
226
|
-
all_diagnostics = await client.call_tool(
|
|
227
|
-
"lean_diagnostic_messages",
|
|
228
|
-
{"file_path": str(declaration_diagnostic_file)},
|
|
229
|
-
)
|
|
230
|
-
all_diag_text = result_text(all_diagnostics)
|
|
231
|
-
|
|
232
|
-
# File should have diagnostics (contains intentional errors)
|
|
233
|
-
assert len(all_diag_text) > 0
|
|
234
|
-
|
|
235
|
-
# Get diagnostics for secondTheorem (has type error in statement)
|
|
161
|
+
# Test 3: Get diagnostics for secondTheorem (has type error in statement)
|
|
236
162
|
diagnostics = await client.call_tool(
|
|
237
163
|
"lean_diagnostic_messages",
|
|
238
164
|
{
|
|
@@ -241,20 +167,10 @@ async def test_diagnostic_messages_with_declaration_name_with_errors(
|
|
|
241
167
|
},
|
|
242
168
|
)
|
|
243
169
|
diag_text = result_text(diagnostics)
|
|
244
|
-
|
|
245
|
-
# secondTheorem has type errors, should have diagnostics
|
|
246
170
|
assert len(diag_text) > 0
|
|
247
171
|
assert isinstance(diag_text, str)
|
|
248
172
|
|
|
249
|
-
|
|
250
|
-
@pytest.mark.asyncio
|
|
251
|
-
async def test_diagnostic_messages_with_declaration_name_no_errors(
|
|
252
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
253
|
-
declaration_diagnostic_file: Path,
|
|
254
|
-
) -> None:
|
|
255
|
-
"""Test filtering by declaration that has no errors."""
|
|
256
|
-
async with mcp_client_factory() as client:
|
|
257
|
-
# Get diagnostics for validFunction (no errors)
|
|
173
|
+
# Test 4: Get diagnostics for validFunction (no errors)
|
|
258
174
|
diagnostics = await client.call_tool(
|
|
259
175
|
"lean_diagnostic_messages",
|
|
260
176
|
{
|
|
@@ -263,8 +179,6 @@ async def test_diagnostic_messages_with_declaration_name_no_errors(
|
|
|
263
179
|
},
|
|
264
180
|
)
|
|
265
181
|
diag_text = result_text(diagnostics)
|
|
266
|
-
|
|
267
|
-
# Should indicate no errors or be empty
|
|
268
182
|
assert (
|
|
269
183
|
"no" in diag_text.lower()
|
|
270
184
|
or len(diag_text.strip()) == 0
|
|
@@ -273,13 +187,13 @@ async def test_diagnostic_messages_with_declaration_name_no_errors(
|
|
|
273
187
|
|
|
274
188
|
|
|
275
189
|
@pytest.mark.asyncio
|
|
276
|
-
async def
|
|
190
|
+
async def test_diagnostic_messages_declaration_edge_cases(
|
|
277
191
|
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
278
192
|
declaration_diagnostic_file: Path,
|
|
279
193
|
) -> None:
|
|
280
|
-
"""Test
|
|
194
|
+
"""Test edge cases for declaration-based filtering."""
|
|
281
195
|
async with mcp_client_factory() as client:
|
|
282
|
-
#
|
|
196
|
+
# Test 1: Non-existent declaration
|
|
283
197
|
result = await client.call_tool(
|
|
284
198
|
"lean_diagnostic_messages",
|
|
285
199
|
{
|
|
@@ -288,21 +202,10 @@ async def test_diagnostic_messages_with_nonexistent_declaration(
|
|
|
288
202
|
},
|
|
289
203
|
)
|
|
290
204
|
result_str = result_text(result)
|
|
291
|
-
|
|
292
|
-
# Should return error message about declaration not found
|
|
293
205
|
assert "not found" in result_str.lower()
|
|
294
206
|
assert "nonExistentTheorem" in result_str
|
|
295
207
|
|
|
296
|
-
|
|
297
|
-
@pytest.mark.asyncio
|
|
298
|
-
async def test_diagnostic_messages_declaration_name_takes_precedence(
|
|
299
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
300
|
-
declaration_diagnostic_file: Path,
|
|
301
|
-
) -> None:
|
|
302
|
-
"""Test that declaration_name takes precedence over start_line/end_line."""
|
|
303
|
-
async with mcp_client_factory() as client:
|
|
304
|
-
# Use declaration_name with conflicting start_line/end_line
|
|
305
|
-
# declaration_name should take precedence
|
|
208
|
+
# Test 2: declaration_name takes precedence over start_line/end_line
|
|
306
209
|
diagnostics = await client.call_tool(
|
|
307
210
|
"lean_diagnostic_messages",
|
|
308
211
|
{
|
|
@@ -313,7 +216,44 @@ async def test_diagnostic_messages_declaration_name_takes_precedence(
|
|
|
313
216
|
},
|
|
314
217
|
)
|
|
315
218
|
diag_text = result_text(diagnostics)
|
|
316
|
-
|
|
317
219
|
# Should get diagnostics for firstTheorem, not lines 1-3
|
|
318
|
-
# (which would only include imports)
|
|
319
220
|
assert len(diag_text) > 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.fixture(scope="module")
|
|
224
|
+
def kernel_error_file(test_project_path: Path) -> Path:
|
|
225
|
+
"""File with kernel error as first error (issue #63)."""
|
|
226
|
+
path = test_project_path / "KernelErrorTest.lean"
|
|
227
|
+
content = textwrap.dedent(
|
|
228
|
+
"""
|
|
229
|
+
import Mathlib.Data.Real.Basic
|
|
230
|
+
|
|
231
|
+
structure test where
|
|
232
|
+
x : ℝ
|
|
233
|
+
deriving Repr
|
|
234
|
+
|
|
235
|
+
lemma test_lemma : False := by rfl
|
|
236
|
+
"""
|
|
237
|
+
).strip()
|
|
238
|
+
if not path.exists() or path.read_text(encoding="utf-8") != content + "\n":
|
|
239
|
+
path.write_text(content + "\n", encoding="utf-8")
|
|
240
|
+
return path
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@pytest.mark.asyncio
|
|
244
|
+
async def test_diagnostic_messages_detects_kernel_errors(
|
|
245
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
246
|
+
kernel_error_file: Path,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Test kernel errors detected when first in file (issue #63)."""
|
|
249
|
+
async with mcp_client_factory() as client:
|
|
250
|
+
diagnostics = await client.call_tool(
|
|
251
|
+
"lean_diagnostic_messages",
|
|
252
|
+
{"file_path": str(kernel_error_file)},
|
|
253
|
+
)
|
|
254
|
+
diag_text = result_text(diagnostics)
|
|
255
|
+
|
|
256
|
+
# Detect kernel error and regular error
|
|
257
|
+
assert "kernel" in diag_text.lower() or "unsafe" in diag_text.lower()
|
|
258
|
+
assert "rfl" in diag_text.lower() or "failed" in diag_text.lower()
|
|
259
|
+
assert diag_text.count("severity") >= 2
|
|
@@ -25,19 +25,34 @@ theorem cachedTheorem : cachedValue = 42 := by rfl
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@pytest.mark.asyncio
|
|
28
|
-
async def
|
|
28
|
+
async def test_file_caching(
|
|
29
29
|
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
30
30
|
cache_test_file: Path,
|
|
31
31
|
) -> None:
|
|
32
|
-
"""
|
|
32
|
+
"""Test file caching: disk changes detected and tools share state correctly."""
|
|
33
33
|
|
|
34
34
|
async with mcp_client_factory() as client:
|
|
35
|
+
# Test 1: Multiple tools share file state correctly
|
|
36
|
+
await client.call_tool(
|
|
37
|
+
"lean_diagnostic_messages", {"file_path": str(cache_test_file)}
|
|
38
|
+
)
|
|
39
|
+
await client.call_tool(
|
|
40
|
+
"lean_goal", {"file_path": str(cache_test_file), "line": 5}
|
|
41
|
+
)
|
|
42
|
+
hover = await client.call_tool(
|
|
43
|
+
"lean_hover_info",
|
|
44
|
+
{"file_path": str(cache_test_file), "line": 3, "column": 5},
|
|
45
|
+
)
|
|
46
|
+
assert "cachedValue" in result_text(hover)
|
|
47
|
+
|
|
48
|
+
# Test 2: Disk changes are detected and reprocessed correctly
|
|
35
49
|
goal1 = await client.call_tool(
|
|
36
50
|
"lean_goal", {"file_path": str(cache_test_file), "line": 5}
|
|
37
51
|
)
|
|
38
52
|
result1 = result_text(goal1)
|
|
39
53
|
assert "no goals" in result1.lower()
|
|
40
54
|
|
|
55
|
+
# Modify file on disk
|
|
41
56
|
cache_test_file.write_text(
|
|
42
57
|
"""import Mathlib
|
|
43
58
|
|
|
@@ -48,6 +63,7 @@ theorem cachedTheorem : cachedValue = 42 := by sorry
|
|
|
48
63
|
encoding="utf-8",
|
|
49
64
|
)
|
|
50
65
|
|
|
66
|
+
# Verify change is detected
|
|
51
67
|
goal2 = await client.call_tool(
|
|
52
68
|
"lean_goal", {"file_path": str(cache_test_file), "line": 5}
|
|
53
69
|
)
|
|
@@ -56,25 +72,3 @@ theorem cachedTheorem : cachedValue = 42 := by sorry
|
|
|
56
72
|
assert "cachedValue = 42" in result2, (
|
|
57
73
|
f"Should show goal at sorry, got: {result2}"
|
|
58
74
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@pytest.mark.asyncio
|
|
62
|
-
async def test_multiple_tools_share_file(
|
|
63
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
64
|
-
cache_test_file: Path,
|
|
65
|
-
) -> None:
|
|
66
|
-
"""Different tools must reuse cached file state correctly."""
|
|
67
|
-
|
|
68
|
-
async with mcp_client_factory() as client:
|
|
69
|
-
await client.call_tool(
|
|
70
|
-
"lean_diagnostic_messages", {"file_path": str(cache_test_file)}
|
|
71
|
-
)
|
|
72
|
-
await client.call_tool(
|
|
73
|
-
"lean_goal", {"file_path": str(cache_test_file), "line": 5}
|
|
74
|
-
)
|
|
75
|
-
hover = await client.call_tool(
|
|
76
|
-
"lean_hover_info",
|
|
77
|
-
{"file_path": str(cache_test_file), "line": 3, "column": 5},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
assert "cachedValue" in result_text(hover)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|