p4r5kpftnp-cmd commited on
Commit
6cf7e7c
·
1 Parent(s): a8eac5b

Add CI/CD: GitHub Actions workflow + ruff config

Browse files

New .github/workflows/ci.yml with three jobs:
- lint: ruff check on every push/PR (fast, no project deps)
- test: pytest over the ~98-test suite on every push/PR
- deploy: on push to main only, pushes to the HF Space (gated on an
HF_TOKEN secret — skips cleanly if the secret isn't set)

Supporting changes to make the suite CI-green:
- ruff.toml: line-length 120; ignore E402 (sys.path.insert-before-import
is intentional in tests/scripts) and E741; exclude web/, data/, .claude/.
- Fixed test isolation: test_proof_agent.py and test_app_fuzz.py
permanently overwrote sys.modules["langgraph_agent"] with a mock and
never restored it, poisoning every later test file (31 failures in a
full run). They now restore sys.modules after importing their target.
- test_retriever_fuzz.py: force a fresh `retriever` import so a cached
real module from another test file can't shadow its mocks.
- test_lean_verifier.py: skipUnless(`lean` on PATH) so the 3 real-Lean
tests skip on CI runners instead of erroring.
- Updated stale assertions: proof_agent default model is now
llama-3.3-70b-versatile; retrieve() returns [] (not RuntimeError) when
no index exists.
- ruff --fix applied across src/scripts/tests (import sorting, trailing
whitespace, unused imports). Full suite: 98 passed locally.
- .gitignore for __pycache__, benchmark CSVs, caches.

.github/workflows/ci.yml ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ name: Lint (ruff)
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Install ruff
19
+ run: pip install ruff
20
+ - name: ruff check
21
+ run: ruff check .
22
+
23
+ test:
24
+ name: Test (pytest)
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+ cache: pip
32
+ - name: Install dependencies
33
+ run: |
34
+ python -m pip install --upgrade pip
35
+ pip install -r requirements.txt
36
+ pip install pytest
37
+ - name: Run pytest
38
+ # The 3 real-Lean tests in test_lean_verifier.py self-skip when no
39
+ # `lean` toolchain is on PATH (CI runners have none); the other ~95
40
+ # tests use mocks and run everywhere.
41
+ run: pytest tests/ -v
42
+
43
+ deploy:
44
+ name: Deploy to Hugging Face Spaces
45
+ needs: [lint, test]
46
+ # Only deploy on a real push to main — never on pull requests.
47
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - name: Check HF_TOKEN is configured
51
+ id: gate
52
+ run: |
53
+ if [ -n "${{ secrets.HF_TOKEN }}" ]; then
54
+ echo "ok=true" >> "$GITHUB_OUTPUT"
55
+ else
56
+ echo "ok=false" >> "$GITHUB_OUTPUT"
57
+ echo "::notice::HF_TOKEN secret not set — skipping Hugging Face Spaces deploy."
58
+ fi
59
+ - name: Checkout (with Git LFS)
60
+ if: steps.gate.outputs.ok == 'true'
61
+ uses: actions/checkout@v4
62
+ with:
63
+ lfs: true
64
+ fetch-depth: 0
65
+ - name: Push to Hugging Face Spaces
66
+ if: steps.gate.outputs.ok == 'true'
67
+ run: |
68
+ git push --force \
69
+ "https://Ray5th:${{ secrets.HF_TOKEN }}@huggingface.co/spaces/Ray5th/lean4-helper" \
70
+ HEAD:main
.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+
8
+ # Claude Code
9
+ .claude/
10
+
11
+ # Benchmark run artifacts
12
+ benchmark_*.csv
13
+ benchmark_results.csv
14
+
15
+ # OS
16
+ .DS_Store
app.py CHANGED
@@ -12,7 +12,6 @@ from langgraph_agent import LangGraphAgent
12
  from lean_verifier import LeanEnvironment
13
  from retriever import MathLibRetriever
14
 
15
-
16
  # ---------------------------------------------------------------------------
17
  # Cached heavyweight components.
18
  #
 
12
  from lean_verifier import LeanEnvironment
13
  from retriever import MathLibRetriever
14
 
 
15
  # ---------------------------------------------------------------------------
16
  # Cached heavyweight components.
17
  #
ruff.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ruff configuration for lean4-helper.
2
+ line-length = 120
3
+
4
+ # Untracked / generated dirs that shouldn't be linted.
5
+ exclude = [
6
+ "web",
7
+ ".claude",
8
+ "__pycache__",
9
+ "data",
10
+ ]
11
+
12
+ [lint]
13
+ # Default rule set (pyflakes F + pycodestyle E/W) plus import sorting.
14
+ select = ["E", "F", "W", "I"]
15
+
16
+ # E402 (module import not at top of file) is intentional throughout: test
17
+ # files and scripts run `sys.path.insert(0, "src")` before importing local
18
+ # modules. E741 (ambiguous name `l`) is a cosmetic nit we accept.
19
+ ignore = ["E402", "E741"]
scripts/benchmark.py CHANGED
@@ -225,8 +225,12 @@ def main():
225
  parser.add_argument("--index-dir", default=None, help="Path to pre-built FAISS index")
226
  parser.add_argument("--output", default="benchmark_results.csv", help="CSV output path")
227
  parser.add_argument("--verbose", action="store_true", help="Print per-problem results")
228
- parser.add_argument("--api-key", default=None, help="API key for the chosen provider (Anthropic for Claude models). Falls back to ANTHROPIC_API_KEY / GROQ_API_KEY env.")
229
- parser.add_argument("--problems-dir", default=None, help="Use local .lean files in this directory instead of MiniF2F. Each file is one problem.")
 
 
 
 
230
  args = parser.parse_args()
231
 
232
  if args.problems_dir:
 
225
  parser.add_argument("--index-dir", default=None, help="Path to pre-built FAISS index")
226
  parser.add_argument("--output", default="benchmark_results.csv", help="CSV output path")
227
  parser.add_argument("--verbose", action="store_true", help="Print per-problem results")
228
+ parser.add_argument("--api-key", default=None,
229
+ help="API key for the chosen provider (Anthropic for Claude models). "
230
+ "Falls back to ANTHROPIC_API_KEY / GROQ_API_KEY env.")
231
+ parser.add_argument("--problems-dir", default=None,
232
+ help="Use local .lean files in this directory instead of MiniF2F. "
233
+ "Each file is one problem.")
234
  args = parser.parse_args()
235
 
236
  if args.problems_dir:
scripts/build_index.py CHANGED
@@ -10,9 +10,9 @@ Optional flags:
10
  --max-files N Limit to first N .lean files (useful for quick testing)
11
  --index-dir PATH Where to save the index (default: data/mathlib_index)
12
  """
13
- import sys
14
- import os
15
  import argparse
 
 
16
 
17
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
18
 
 
10
  --max-files N Limit to first N .lean files (useful for quick testing)
11
  --index-dir PATH Where to save the index (default: data/mathlib_index)
12
  """
 
 
13
  import argparse
14
+ import os
15
+ import sys
16
 
17
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
18
 
scripts/run_agent.py CHANGED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
- import sys
3
- import os
4
  import argparse
 
 
5
 
6
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
7
 
 
1
  #!/usr/bin/env python3
 
 
2
  import argparse
3
+ import os
4
+ import sys
5
 
6
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
7
 
src/langgraph_agent.py CHANGED
@@ -3,12 +3,10 @@ import re
3
  from typing import List, TypedDict
4
 
5
  from langgraph.graph import END, StateGraph
6
-
7
  from lean_verifier import LeanEnvironment
8
  from rag_chain import RAGProofChain
9
  from retriever import MathLibRetriever
10
 
11
-
12
  # ---------------------------------------------------------------------------
13
  # State
14
  # ---------------------------------------------------------------------------
 
3
  from typing import List, TypedDict
4
 
5
  from langgraph.graph import END, StateGraph
 
6
  from lean_verifier import LeanEnvironment
7
  from rag_chain import RAGProofChain
8
  from retriever import MathLibRetriever
9
 
 
10
  # ---------------------------------------------------------------------------
11
  # State
12
  # ---------------------------------------------------------------------------
src/lean_verifier.py CHANGED
@@ -1,7 +1,7 @@
1
  import threading
2
- from typing import Dict, Any, List
3
 
4
- from lean_interact import LeanREPLConfig, LeanServer, Command, TempRequireProject, LeanRequire
5
 
6
 
7
  class LeanEnvironment:
@@ -16,7 +16,7 @@ class LeanEnvironment:
16
  def __init__(self, use_mathlib: bool = True, lean_version: str = "v4.8.0"):
17
  """
18
  Initializes the Lean environment.
19
-
20
  Args:
21
  use_mathlib (bool): If True, configures a TempRequireProject with Mathlib.
22
  This may take a while to build on the first run.
@@ -28,7 +28,7 @@ class LeanEnvironment:
28
  if self.use_mathlib:
29
  # We use TempRequireProject with mathlib as specified in lean_interact documentation
30
  project = TempRequireProject(
31
- lean_version=self.lean_version,
32
  require="mathlib"
33
  )
34
  self.config = LeanREPLConfig(project=project)
@@ -74,20 +74,20 @@ class LeanEnvironment:
74
 
75
  errors = []
76
  goals = []
77
-
78
  # Check for error or warning messages
79
  if hasattr(response, 'messages') and response.messages:
80
  for msg in response.messages:
81
  if msg.severity in ['error', 'warning']:
82
  # E.g., 'declaration uses 'sorry'' is a warning, but we might want to capture it
83
  errors.append(msg.data)
84
-
85
  # Check for open goals (sorries)
86
  if hasattr(response, 'sorries') and response.sorries:
87
  for sorry in response.sorries:
88
  if sorry.goal:
89
  goals.append(sorry.goal)
90
-
91
  is_success = len(errors) == 0 and len(goals) == 0
92
 
93
  return {
 
1
  import threading
2
+ from typing import Any, Dict
3
 
4
+ from lean_interact import Command, LeanREPLConfig, LeanServer, TempRequireProject
5
 
6
 
7
  class LeanEnvironment:
 
16
  def __init__(self, use_mathlib: bool = True, lean_version: str = "v4.8.0"):
17
  """
18
  Initializes the Lean environment.
19
+
20
  Args:
21
  use_mathlib (bool): If True, configures a TempRequireProject with Mathlib.
22
  This may take a while to build on the first run.
 
28
  if self.use_mathlib:
29
  # We use TempRequireProject with mathlib as specified in lean_interact documentation
30
  project = TempRequireProject(
31
+ lean_version=self.lean_version,
32
  require="mathlib"
33
  )
34
  self.config = LeanREPLConfig(project=project)
 
74
 
75
  errors = []
76
  goals = []
77
+
78
  # Check for error or warning messages
79
  if hasattr(response, 'messages') and response.messages:
80
  for msg in response.messages:
81
  if msg.severity in ['error', 'warning']:
82
  # E.g., 'declaration uses 'sorry'' is a warning, but we might want to capture it
83
  errors.append(msg.data)
84
+
85
  # Check for open goals (sorries)
86
  if hasattr(response, 'sorries') and response.sorries:
87
  for sorry in response.sorries:
88
  if sorry.goal:
89
  goals.append(sorry.goal)
90
+
91
  is_success = len(errors) == 0 and len(goals) == 0
92
 
93
  return {
src/lmm_client.py CHANGED
@@ -1,6 +1,7 @@
1
- from groq import Groq
2
  from typing import List, Optional
3
 
 
 
4
 
5
  class LMMClient:
6
  """
 
 
1
  from typing import List, Optional
2
 
3
+ from groq import Groq
4
+
5
 
6
  class LMMClient:
7
  """
src/mathlib_corpus.py CHANGED
@@ -1,12 +1,11 @@
 
1
  import os
2
  import re
3
- import glob
4
  from pathlib import Path
5
  from typing import List, Optional
6
 
7
  from langchain_core.documents import Document
8
 
9
-
10
  # Regex to capture optional docstring + declaration line
11
  _DECL_PATTERN = re.compile(
12
  r'(?:/--\s*(.*?)\s*-/)?\s*' # optional /-- docstring -/
 
1
+ import glob
2
  import os
3
  import re
 
4
  from pathlib import Path
5
  from typing import List, Optional
6
 
7
  from langchain_core.documents import Document
8
 
 
9
  # Regex to capture optional docstring + declaration line
10
  _DECL_PATTERN = re.compile(
11
  r'(?:/--\s*(.*?)\s*-/)?\s*' # optional /-- docstring -/
src/rag_chain.py CHANGED
@@ -1,14 +1,14 @@
1
  import subprocess
2
  from typing import Any, Dict, List, Optional
3
 
 
4
  from langchain_core.documents import Document
5
  from langchain_core.language_models.chat_models import BaseChatModel
6
  from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
7
- from langchain_core.outputs import ChatGeneration, ChatResult
8
  from langchain_core.output_parsers import StrOutputParser
 
9
  from langchain_core.prompts import ChatPromptTemplate
10
  from langchain_groq import ChatGroq
11
- from langchain_anthropic import ChatAnthropic
12
 
13
 
14
  class ClaudeCliChat(BaseChatModel):
 
1
  import subprocess
2
  from typing import Any, Dict, List, Optional
3
 
4
+ from langchain_anthropic import ChatAnthropic
5
  from langchain_core.documents import Document
6
  from langchain_core.language_models.chat_models import BaseChatModel
7
  from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
 
8
  from langchain_core.output_parsers import StrOutputParser
9
+ from langchain_core.outputs import ChatGeneration, ChatResult
10
  from langchain_core.prompts import ChatPromptTemplate
11
  from langchain_groq import ChatGroq
 
12
 
13
 
14
  class ClaudeCliChat(BaseChatModel):
src/retriever.py CHANGED
@@ -1,15 +1,13 @@
1
- import os
2
  from pathlib import Path
3
  from typing import List, Optional
4
 
 
 
 
5
  from langchain_community.retrievers import BM25Retriever
6
  from langchain_community.vectorstores import FAISS
7
- from langchain_community.cross_encoders import HuggingFaceCrossEncoder
8
- from langchain_classic.retrievers import EnsembleRetriever, ContextualCompressionRetriever
9
- from langchain_classic.retrievers.document_compressors import CrossEncoderReranker
10
- from langchain_huggingface import HuggingFaceEmbeddings
11
  from langchain_core.documents import Document
12
-
13
  from mathlib_corpus import MathLibCorpus
14
 
15
  _DEFAULT_INDEX_DIR = Path(__file__).resolve().parent.parent / "data" / "mathlib_index"
 
 
1
  from pathlib import Path
2
  from typing import List, Optional
3
 
4
+ from langchain_classic.retrievers import ContextualCompressionRetriever, EnsembleRetriever
5
+ from langchain_classic.retrievers.document_compressors import CrossEncoderReranker
6
+ from langchain_community.cross_encoders import HuggingFaceCrossEncoder
7
  from langchain_community.retrievers import BM25Retriever
8
  from langchain_community.vectorstores import FAISS
 
 
 
 
9
  from langchain_core.documents import Document
10
+ from langchain_huggingface import HuggingFaceEmbeddings
11
  from mathlib_corpus import MathLibCorpus
12
 
13
  _DEFAULT_INDEX_DIR = Path(__file__).resolve().parent.parent / "data" / "mathlib_index"
tests/test_app_fuzz.py CHANGED
@@ -13,7 +13,6 @@ We mock `langgraph_agent.LangGraphAgent` BEFORE importing `app` so no Lean,
13
  LLM, or FAISS infrastructure is needed.
14
  """
15
 
16
- import io
17
  import os
18
  import sys
19
  import tempfile
@@ -67,13 +66,24 @@ class _DummyAgent:
67
 
68
 
69
  _fake_lg_module.LangGraphAgent = _DummyAgent
70
- sys.modules["langgraph_agent"] = _fake_lg_module
71
 
72
  # Default to having a key set; individual tests override this.
73
  os.environ.setdefault("GROQ_API_KEY", "test-key")
74
 
 
 
 
 
 
 
75
  import app # noqa: E402 (must come after the mock above)
76
 
 
 
 
 
 
 
77
 
78
  # Sentinels for the fuzz corpus -----------------------------------------------
79
  EMPTY = ""
 
13
  LLM, or FAISS infrastructure is needed.
14
  """
15
 
 
16
  import os
17
  import sys
18
  import tempfile
 
66
 
67
 
68
  _fake_lg_module.LangGraphAgent = _DummyAgent
 
69
 
70
  # Default to having a key set; individual tests override this.
71
  os.environ.setdefault("GROQ_API_KEY", "test-key")
72
 
73
+ # Mock langgraph_agent only for the duration of importing `app`, then restore
74
+ # sys.modules so this file doesn't poison the import for other test files.
75
+ # app.py captures its own `LangGraphAgent` reference at import time; tests
76
+ # patch `app.LangGraphAgent` directly afterward.
77
+ _orig_langgraph_agent = sys.modules.get("langgraph_agent")
78
+ sys.modules["langgraph_agent"] = _fake_lg_module
79
  import app # noqa: E402 (must come after the mock above)
80
 
81
+ if _orig_langgraph_agent is not None:
82
+ sys.modules["langgraph_agent"] = _orig_langgraph_agent
83
+ else:
84
+ del sys.modules["langgraph_agent"]
85
+ app.LangGraphAgent = _DummyAgent # keep app bound to the dummy after restore
86
+
87
 
88
  # Sentinels for the fuzz corpus -----------------------------------------------
89
  EMPTY = ""
tests/test_lean_verifier.py CHANGED
@@ -1,12 +1,20 @@
1
- import unittest
2
- import sys
3
  import os
 
 
 
4
 
5
  # Add src to Python path
6
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
7
 
8
  from lean_verifier import LeanEnvironment
9
 
 
 
 
 
 
 
 
10
  class TestLeanVerifier(unittest.TestCase):
11
  @classmethod
12
  def setUpClass(cls):
@@ -34,7 +42,7 @@ theorem add_comm_test (n m : Nat) : n + m = m + n := by
34
  """
35
  result = self.lean_env.verify_proof(lean_code)
36
  self.assertEqual(result["status"], "failure")
37
- self.assertTrue(any("type mismatch" in err or "application type mismatch" in err for err in result["errors"]),
38
  f"Expected type mismatch error, got: {result['errors']}")
39
 
40
  def test_incomplete_proof_sorry(self):
@@ -47,7 +55,7 @@ theorem my_incomplete_thm (n : Nat) : n = 5 → n = 5 := by
47
  result = self.lean_env.verify_proof(lean_code)
48
  self.assertEqual(result["status"], "failure")
49
  # Ensure it has an error indicating sorry
50
- self.assertTrue(any("uses 'sorry'" in err for err in result["errors"]),
51
  f"Expected sorry warning/error, got: {result['errors']}")
52
  # Ensure it outputs the goal
53
  self.assertEqual(len(result["goals"]), 1)
 
 
 
1
  import os
2
+ import shutil
3
+ import sys
4
+ import unittest
5
 
6
  # Add src to Python path
7
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
8
 
9
  from lean_verifier import LeanEnvironment
10
 
11
+ # These tests drive a real Lean REPL + Mathlib. CI runners (and most dev
12
+ # machines without elan) have no `lean` on PATH, so skip cleanly there
13
+ # instead of erroring in setUpClass.
14
+ LEAN_AVAILABLE = shutil.which("lean") is not None
15
+
16
+
17
+ @unittest.skipUnless(LEAN_AVAILABLE, "Lean toolchain not installed (no `lean` on PATH)")
18
  class TestLeanVerifier(unittest.TestCase):
19
  @classmethod
20
  def setUpClass(cls):
 
42
  """
43
  result = self.lean_env.verify_proof(lean_code)
44
  self.assertEqual(result["status"], "failure")
45
+ self.assertTrue(any("type mismatch" in err or "application type mismatch" in err for err in result["errors"]),
46
  f"Expected type mismatch error, got: {result['errors']}")
47
 
48
  def test_incomplete_proof_sorry(self):
 
55
  result = self.lean_env.verify_proof(lean_code)
56
  self.assertEqual(result["status"], "failure")
57
  # Ensure it has an error indicating sorry
58
+ self.assertTrue(any("uses 'sorry'" in err for err in result["errors"]),
59
  f"Expected sorry warning/error, got: {result['errors']}")
60
  # Ensure it outputs the goal
61
  self.assertEqual(len(result["goals"]), 1)
tests/test_manual_bug_fixes.py CHANGED
@@ -27,6 +27,7 @@ for _mod in ("lean_verifier", "retriever", "rag_chain"):
27
  sys.modules[_mod] = mock.MagicMock()
28
 
29
  import langgraph_agent
 
30
  importlib.reload(langgraph_agent) # ensure regex/keyword changes are picked up
31
  _count_theorem_blocks = langgraph_agent._count_theorem_blocks
32
  _extract_lean_code = langgraph_agent._extract_lean_code
@@ -71,7 +72,7 @@ class TestCountTheoremBlocksKeywordFix(unittest.TestCase):
71
 
72
 
73
  class TestExtractLeanCodeFenceVariants(unittest.TestCase):
74
- """
75
  `_extract_lean_code` used to do `text.split("```lean")[1]` which would
76
  leave a `4` at the start when the fence was `\`\`\`lean4` (a common LLM
77
  output). Now uses a regex that requires the lean tag to be terminated by
@@ -142,6 +143,7 @@ class TestProofAgentDefaultModel(unittest.TestCase):
142
  # Inspect the function default directly — avoids needing a working
143
  # LangGraphAgent mock chain.
144
  import inspect
 
145
  import proof_agent
146
 
147
  sig = inspect.signature(proof_agent.ProofAgent.__init__)
 
27
  sys.modules[_mod] = mock.MagicMock()
28
 
29
  import langgraph_agent
30
+
31
  importlib.reload(langgraph_agent) # ensure regex/keyword changes are picked up
32
  _count_theorem_blocks = langgraph_agent._count_theorem_blocks
33
  _extract_lean_code = langgraph_agent._extract_lean_code
 
72
 
73
 
74
  class TestExtractLeanCodeFenceVariants(unittest.TestCase):
75
+ r"""
76
  `_extract_lean_code` used to do `text.split("```lean")[1]` which would
77
  leave a `4` at the start when the fence was `\`\`\`lean4` (a common LLM
78
  output). Now uses a regex that requires the lean tag to be terminated by
 
143
  # Inspect the function default directly — avoids needing a working
144
  # LangGraphAgent mock chain.
145
  import inspect
146
+
147
  import proof_agent
148
 
149
  sig = inspect.signature(proof_agent.ProofAgent.__init__)
tests/test_problems_parse.py CHANGED
@@ -9,7 +9,6 @@ No Lean install required - these tests only exercise text-level helpers:
9
  - _extract_lean_code (idempotent on raw .lean files with no fences)
10
  """
11
 
12
- import os
13
  import sys
14
  import unittest
15
  from pathlib import Path
 
9
  - _extract_lean_code (idempotent on raw .lean files with no fences)
10
  """
11
 
 
12
  import sys
13
  import unittest
14
  from pathlib import Path
tests/test_proof_agent.py CHANGED
@@ -1,30 +1,38 @@
1
- import unittest
2
- import sys
3
  import os
4
- from unittest.mock import patch, MagicMock
 
 
5
 
6
  # Add src to Python path
7
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
8
 
9
- # Mock langgraph_agent.LangGraphAgent before importing proof_agent
 
 
 
 
10
  sys.modules['langgraph_agent'] = MagicMock()
11
 
12
- import proof_agent
13
  from proof_agent import ProofAgent
14
 
 
 
 
 
 
15
 
16
  class TestProofAgentConstructor(unittest.TestCase):
17
  def test_default_args_constructs_langgraph_agent_with_defaults(self):
18
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
19
- agent = ProofAgent()
20
  MockLangGraphAgent.assert_called_once_with(
21
- model_name="qwen3-vl:4b",
22
  max_retries=5,
23
  )
24
 
25
  def test_custom_model_name_forwarded(self):
26
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
27
- agent = ProofAgent(model_name="custom-model:7b")
28
  MockLangGraphAgent.assert_called_once_with(
29
  model_name="custom-model:7b",
30
  max_retries=5,
@@ -32,15 +40,15 @@ class TestProofAgentConstructor(unittest.TestCase):
32
 
33
  def test_custom_max_retries_forwarded(self):
34
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
35
- agent = ProofAgent(max_retries=10)
36
  MockLangGraphAgent.assert_called_once_with(
37
- model_name="qwen3-vl:4b",
38
  max_retries=10,
39
  )
40
 
41
  def test_all_custom_args_forwarded(self):
42
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
43
- agent = ProofAgent(model_name="other-model:13b", max_retries=3)
44
  MockLangGraphAgent.assert_called_once_with(
45
  model_name="other-model:13b",
46
  max_retries=3,
 
 
 
1
  import os
2
+ import sys
3
+ import unittest
4
+ from unittest.mock import MagicMock, patch
5
 
6
  # Add src to Python path
7
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
8
 
9
+ # Mock langgraph_agent.LangGraphAgent before importing proof_agent (so its
10
+ # top-level `from langgraph_agent import LangGraphAgent` doesn't pull in the
11
+ # heavy real module). Restore sys.modules afterward so this file doesn't
12
+ # poison the langgraph_agent import for other test files in the same run.
13
+ _orig_langgraph_agent = sys.modules.get('langgraph_agent')
14
  sys.modules['langgraph_agent'] = MagicMock()
15
 
 
16
  from proof_agent import ProofAgent
17
 
18
+ if _orig_langgraph_agent is not None:
19
+ sys.modules['langgraph_agent'] = _orig_langgraph_agent
20
+ else:
21
+ del sys.modules['langgraph_agent']
22
+
23
 
24
  class TestProofAgentConstructor(unittest.TestCase):
25
  def test_default_args_constructs_langgraph_agent_with_defaults(self):
26
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
27
+ ProofAgent()
28
  MockLangGraphAgent.assert_called_once_with(
29
+ model_name="llama-3.3-70b-versatile",
30
  max_retries=5,
31
  )
32
 
33
  def test_custom_model_name_forwarded(self):
34
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
35
+ ProofAgent(model_name="custom-model:7b")
36
  MockLangGraphAgent.assert_called_once_with(
37
  model_name="custom-model:7b",
38
  max_retries=5,
 
40
 
41
  def test_custom_max_retries_forwarded(self):
42
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
43
+ ProofAgent(max_retries=10)
44
  MockLangGraphAgent.assert_called_once_with(
45
+ model_name="llama-3.3-70b-versatile",
46
  max_retries=10,
47
  )
48
 
49
  def test_all_custom_args_forwarded(self):
50
  with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
51
+ ProofAgent(model_name="other-model:13b", max_retries=3)
52
  MockLangGraphAgent.assert_called_once_with(
53
  model_name="other-model:13b",
54
  max_retries=3,
tests/test_retriever_fuzz.py CHANGED
@@ -104,7 +104,11 @@ _patchers = [
104
  for p in _patchers:
105
  p.start()
106
 
107
- # Now safe to import the module under test.
 
 
 
 
108
  import retriever # noqa: E402
109
 
110
  # Re-bind the locally imported names inside `retriever` to our fakes, because
@@ -181,13 +185,14 @@ class RetrieveQueryFuzzTests(unittest.TestCase):
181
 
182
 
183
  class RetrieveLazyLoadTests(unittest.TestCase):
184
- """`retrieve()` should lazily call `_load()` when no retriever exists."""
185
 
186
- def test_retrieve_without_built_index_raises_runtime_error(self):
 
 
187
  with tempfile.TemporaryDirectory() as tmp:
188
  r = retriever.MathLibRetriever(index_dir=tmp)
189
- with self.assertRaises(RuntimeError):
190
- r.retrieve("query")
191
 
192
 
193
  class InitFuzzTests(unittest.TestCase):
 
104
  for p in _patchers:
105
  p.start()
106
 
107
+ # Force a fresh import: another test file in the same run may have already
108
+ # imported the real `retriever` (e.g. via `app`), which would shadow our
109
+ # mocked dependencies. Dropping it from sys.modules makes the import below
110
+ # re-run with the patchers active.
111
+ sys.modules.pop("retriever", None)
112
  import retriever # noqa: E402
113
 
114
  # Re-bind the locally imported names inside `retriever` to our fakes, because
 
185
 
186
 
187
  class RetrieveLazyLoadTests(unittest.TestCase):
188
+ """`retrieve()` should degrade gracefully when no index is present."""
189
 
190
+ def test_retrieve_without_built_index_returns_empty(self):
191
+ # The retriever skips RAG (returns []) rather than raising when no
192
+ # FAISS index exists, so the agent can still run LLM-only.
193
  with tempfile.TemporaryDirectory() as tmp:
194
  r = retriever.MathLibRetriever(index_dir=tmp)
195
+ self.assertEqual(r.retrieve("query"), [])
 
196
 
197
 
198
  class InitFuzzTests(unittest.TestCase):
tests/test_state_machine_fuzz.py CHANGED
@@ -7,8 +7,8 @@ Covers:
7
  - make_verify_node(lean_env): behavior with mocked verifier.
8
  """
9
 
10
- import sys
11
  import os
 
12
  import unittest
13
  from unittest.mock import MagicMock, patch
14
 
@@ -16,13 +16,12 @@ from unittest.mock import MagicMock, patch
16
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
17
  sys.path.insert(0, 'src')
18
 
19
- from langgraph.graph import END
20
-
21
  import langgraph_agent
 
22
  from langgraph_agent import (
23
- should_continue,
24
  make_generate_node,
25
  make_verify_node,
 
26
  )
27
 
28
 
 
7
  - make_verify_node(lean_env): behavior with mocked verifier.
8
  """
9
 
 
10
  import os
11
+ import sys
12
  import unittest
13
  from unittest.mock import MagicMock, patch
14
 
 
16
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
17
  sys.path.insert(0, 'src')
18
 
 
 
19
  import langgraph_agent
20
+ from langgraph.graph import END
21
  from langgraph_agent import (
 
22
  make_generate_node,
23
  make_verify_node,
24
+ should_continue,
25
  )
26
 
27