Spaces:
Sleeping
Add CI/CD: GitHub Actions workflow + ruff config
Browse filesNew .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 +70 -0
- .gitignore +16 -0
- app.py +0 -1
- ruff.toml +19 -0
- scripts/benchmark.py +6 -2
- scripts/build_index.py +2 -2
- scripts/run_agent.py +2 -2
- src/langgraph_agent.py +0 -2
- src/lean_verifier.py +7 -7
- src/lmm_client.py +2 -1
- src/mathlib_corpus.py +1 -2
- src/rag_chain.py +2 -2
- src/retriever.py +4 -6
- tests/test_app_fuzz.py +12 -2
- tests/test_lean_verifier.py +12 -4
- tests/test_manual_bug_fixes.py +3 -1
- tests/test_problems_parse.py +0 -1
- tests/test_proof_agent.py +19 -11
- tests/test_retriever_fuzz.py +10 -5
- tests/test_state_machine_fuzz.py +3 -4
|
@@ -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
|
|
@@ -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
|
|
@@ -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 |
#
|
|
@@ -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"]
|
|
@@ -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,
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
@@ -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 |
|
|
@@ -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 |
|
|
@@ -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 |
# ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import threading
|
| 2 |
-
from typing import
|
| 3 |
|
| 4 |
-
from lean_interact import LeanREPLConfig, LeanServer,
|
| 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 {
|
|
@@ -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 |
"""
|
|
@@ -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 -/
|
|
@@ -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):
|
|
@@ -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"
|
|
@@ -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 = ""
|
|
@@ -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)
|
|
@@ -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__)
|
|
@@ -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
|
|
@@ -1,30 +1,38 @@
|
|
| 1 |
-
import unittest
|
| 2 |
-
import sys
|
| 3 |
import os
|
| 4 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 20 |
MockLangGraphAgent.assert_called_once_with(
|
| 21 |
-
model_name="
|
| 22 |
max_retries=5,
|
| 23 |
)
|
| 24 |
|
| 25 |
def test_custom_model_name_forwarded(self):
|
| 26 |
with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
|
| 27 |
-
|
| 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 |
-
|
| 36 |
MockLangGraphAgent.assert_called_once_with(
|
| 37 |
-
model_name="
|
| 38 |
max_retries=10,
|
| 39 |
)
|
| 40 |
|
| 41 |
def test_all_custom_args_forwarded(self):
|
| 42 |
with patch('proof_agent.LangGraphAgent') as MockLangGraphAgent:
|
| 43 |
-
|
| 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,
|
|
@@ -104,7 +104,11 @@ _patchers = [
|
|
| 104 |
for p in _patchers:
|
| 105 |
p.start()
|
| 106 |
|
| 107 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 185 |
|
| 186 |
-
def
|
|
|
|
|
|
|
| 187 |
with tempfile.TemporaryDirectory() as tmp:
|
| 188 |
r = retriever.MathLibRetriever(index_dir=tmp)
|
| 189 |
-
|
| 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):
|
|
@@ -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 |
|