mirror of
https://github.com/codeflash-ai/codeflash-agent.git
synced 2026-05-04 18:25:19 +00:00
Add tests for error handling paths in ranking, refinement, and state
- test_ranking: Update normalize_code test to expect fallback on invalid syntax instead of SyntaxError (matches new behavior) - test_refinement: Add 7 tests for _parse_candidate markdown parsing (fenced blocks, file paths, multiple blocks, plain fallback) - test_state: Add 6 tests for PythonState.module_ast and invalidate_module (valid parse, caching, SyntaxError→None, re-parse after fix)
This commit is contained in:
parent
9e679f1c06
commit
e1990092e0
3 changed files with 184 additions and 4 deletions
|
|
@ -134,10 +134,12 @@ class TestNormalizeCode:
|
|||
|
||||
assert normalize_code(code_a) != normalize_code(code_b)
|
||||
|
||||
def test_invalid_code_raises_syntax_error(self) -> None:
|
||||
"""Invalid Python code raises SyntaxError."""
|
||||
with pytest.raises(SyntaxError):
|
||||
normalize_code("def (broken:")
|
||||
def test_invalid_code_returns_raw_string(self) -> None:
|
||||
"""Invalid Python code falls back to the raw string."""
|
||||
normalize_code.cache_clear()
|
||||
result = normalize_code("def (broken:")
|
||||
|
||||
assert "def (broken:" == result
|
||||
|
||||
|
||||
class TestDiffLength:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from codeflash_python.ai._refinement import (
|
|||
CodeRepairRequest,
|
||||
OptimizedCandidateSource,
|
||||
RefinementRequest,
|
||||
_parse_candidate,
|
||||
adaptive_optimize,
|
||||
code_repair,
|
||||
optimize_code_refinement,
|
||||
|
|
@ -469,3 +470,95 @@ class TestAdaptiveOptimize:
|
|||
client, # type: ignore[arg-type]
|
||||
make_adaptive_request(),
|
||||
)
|
||||
|
||||
|
||||
class TestParseCandidateMarkdown:
|
||||
"""_parse_candidate markdown code-block parsing."""
|
||||
|
||||
def test_plain_code_passes_through(self) -> None:
|
||||
"""Plain code without markdown fences is used as-is."""
|
||||
data = {
|
||||
"source_code": "def foo(): return 1",
|
||||
"explanation": "Plain",
|
||||
"optimization_id": "opt-1",
|
||||
}
|
||||
|
||||
result = _parse_candidate(data, "REPAIR")
|
||||
|
||||
assert result is not None
|
||||
assert "def foo(): return 1" == result.code
|
||||
|
||||
def test_markdown_fenced_code_is_extracted(self) -> None:
|
||||
"""Markdown-fenced code block is parsed and extracted."""
|
||||
data = {
|
||||
"source_code": "```python\ndef foo(): return 2\n```",
|
||||
"explanation": "Fenced",
|
||||
"optimization_id": "opt-2",
|
||||
}
|
||||
|
||||
result = _parse_candidate(data, "REFINE")
|
||||
|
||||
assert result is not None
|
||||
assert "def foo(): return 2" == result.code
|
||||
|
||||
def test_markdown_with_file_path_is_extracted(self) -> None:
|
||||
"""Markdown block with file path suffix is parsed."""
|
||||
data = {
|
||||
"source_code": ("```python:src/mod.py\ndef bar(): pass\n```"),
|
||||
"explanation": "With path",
|
||||
"optimization_id": "opt-3",
|
||||
}
|
||||
|
||||
result = _parse_candidate(data, "REFINE")
|
||||
|
||||
assert result is not None
|
||||
assert "def bar(): pass" == result.code
|
||||
|
||||
def test_multiple_code_blocks_joined(self) -> None:
|
||||
"""Multiple markdown blocks are joined with double newlines."""
|
||||
data = {
|
||||
"source_code": (
|
||||
"```python\ndef a(): pass\n```\n```python\ndef b(): pass\n```"
|
||||
),
|
||||
"explanation": "Multi",
|
||||
"optimization_id": "opt-4",
|
||||
}
|
||||
|
||||
result = _parse_candidate(data, "ADAPTIVE")
|
||||
|
||||
assert result is not None
|
||||
assert "def a(): pass" in result.code
|
||||
assert "def b(): pass" in result.code
|
||||
assert "\n\n" in result.code
|
||||
|
||||
def test_empty_source_code_returns_none(self) -> None:
|
||||
"""Empty source_code returns None."""
|
||||
data = {
|
||||
"source_code": "",
|
||||
"explanation": "Empty",
|
||||
"optimization_id": "opt-5",
|
||||
}
|
||||
|
||||
assert _parse_candidate(data, "REPAIR") is None
|
||||
|
||||
def test_missing_source_code_returns_none(self) -> None:
|
||||
"""Missing source_code key returns None."""
|
||||
data: dict[str, Any] = {
|
||||
"explanation": "No code",
|
||||
"optimization_id": "opt-6",
|
||||
}
|
||||
|
||||
assert _parse_candidate(data, "REPAIR") is None
|
||||
|
||||
def test_source_field_is_forwarded(self) -> None:
|
||||
"""The source parameter is set on the Candidate."""
|
||||
data = {
|
||||
"source_code": "x = 1",
|
||||
"explanation": "Test",
|
||||
"optimization_id": "opt-7",
|
||||
}
|
||||
|
||||
result = _parse_candidate(data, "ADAPTIVE")
|
||||
|
||||
assert result is not None
|
||||
assert "ADAPTIVE" == result.source
|
||||
|
|
|
|||
85
packages/codeflash-python/tests/test_state.py
Normal file
85
packages/codeflash-python/tests/test_state.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Tests for PythonState lazy accessor methods."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from codeflash_python._state import PythonState
|
||||
|
||||
|
||||
@pytest.fixture(name="state")
|
||||
def _state() -> PythonState:
|
||||
"""Build a PythonState with a minimal mock configuration."""
|
||||
cfg = MagicMock()
|
||||
cfg.project_root = Path("/fake/project")
|
||||
return PythonState(cfg=cfg)
|
||||
|
||||
|
||||
class TestModuleAst:
|
||||
"""Tests for PythonState.module_ast."""
|
||||
|
||||
def test_valid_file_returns_ast(self, state, tmp_path) -> None:
|
||||
"""Valid Python source is parsed and cached."""
|
||||
py_file = tmp_path / "mod.py"
|
||||
py_file.write_text("x = 1\n", encoding="utf-8")
|
||||
|
||||
result = state.module_ast(py_file)
|
||||
|
||||
assert isinstance(result, ast.Module)
|
||||
|
||||
def test_cached_on_second_call(self, state, tmp_path) -> None:
|
||||
"""Second call returns the same cached object."""
|
||||
py_file = tmp_path / "mod.py"
|
||||
py_file.write_text("x = 1\n", encoding="utf-8")
|
||||
|
||||
first = state.module_ast(py_file)
|
||||
second = state.module_ast(py_file)
|
||||
|
||||
assert first is second
|
||||
|
||||
def test_syntax_error_returns_none(self, state, tmp_path) -> None:
|
||||
"""Malformed source returns None instead of crashing."""
|
||||
py_file = tmp_path / "bad.py"
|
||||
py_file.write_text("def (broken:\n", encoding="utf-8")
|
||||
|
||||
result = state.module_ast(py_file)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_syntax_error_not_cached(self, state, tmp_path) -> None:
|
||||
"""Failed parse is not cached — a corrected file can be re-read."""
|
||||
py_file = tmp_path / "fix.py"
|
||||
py_file.write_text("def (broken:\n", encoding="utf-8")
|
||||
|
||||
assert state.module_ast(py_file) is None
|
||||
|
||||
py_file.write_text("x = 1\n", encoding="utf-8")
|
||||
result = state.module_ast(py_file)
|
||||
|
||||
assert isinstance(result, ast.Module)
|
||||
|
||||
|
||||
class TestInvalidateModule:
|
||||
"""Tests for PythonState.invalidate_module."""
|
||||
|
||||
def test_invalidate_clears_cached_ast(self, state, tmp_path) -> None:
|
||||
"""After invalidation, the next call re-parses."""
|
||||
py_file = tmp_path / "mod.py"
|
||||
py_file.write_text("x = 1\n", encoding="utf-8")
|
||||
|
||||
first = state.module_ast(py_file)
|
||||
state.invalidate_module(py_file)
|
||||
|
||||
py_file.write_text("y = 2\n", encoding="utf-8")
|
||||
second = state.module_ast(py_file)
|
||||
|
||||
assert first is not second
|
||||
assert "y" in ast.unparse(second)
|
||||
|
||||
def test_invalidate_nonexistent_path_is_noop(self, state) -> None:
|
||||
"""Invalidating a path that was never cached does not raise."""
|
||||
state.invalidate_module(Path("/nonexistent/mod.py"))
|
||||
Loading…
Reference in a new issue