From 2f3b2149d5f6c3a86e4b25d097060179b772e38d Mon Sep 17 00:00:00 2001 From: agentson Date: Sat, 28 Feb 2026 14:35:35 +0900 Subject: [PATCH] fix: add syntax guard for evolved strategy generation (#321) --- src/evolution/optimizer.py | 39 +++++++++++++++++++------------- tests/test_evolution.py | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/evolution/optimizer.py b/src/evolution/optimizer.py index bd4a99b..c9ef719 100644 --- a/src/evolution/optimizer.py +++ b/src/evolution/optimizer.py @@ -9,6 +9,7 @@ This module: from __future__ import annotations +import ast import json import logging import sqlite3 @@ -28,24 +29,24 @@ from src.logging.decision_logger import DecisionLogger logger = logging.getLogger(__name__) STRATEGIES_DIR = Path("src/strategies") -STRATEGY_TEMPLATE = textwrap.dedent("""\ - \"\"\"Auto-generated strategy: {name} +STRATEGY_TEMPLATE = """\ +\"\"\"Auto-generated strategy: {name} - Generated at: {timestamp} - Rationale: {rationale} - \"\"\" +Generated at: {timestamp} +Rationale: {rationale} +\"\"\" - from __future__ import annotations - from typing import Any - from src.strategies.base import BaseStrategy +from __future__ import annotations +from typing import Any +from src.strategies.base import BaseStrategy - class {class_name}(BaseStrategy): - \"\"\"Strategy: {name}\"\"\" +class {class_name}(BaseStrategy): + \"\"\"Strategy: {name}\"\"\" - def evaluate(self, market_data: dict[str, Any]) -> dict[str, Any]: - {body} -""") + def evaluate(self, market_data: dict[str, Any]) -> dict[str, Any]: +{body} +""" class EvolutionOptimizer: @@ -235,7 +236,8 @@ class EvolutionOptimizer: file_path = STRATEGIES_DIR / file_name # Indent the body for the class method - indented_body = textwrap.indent(body, " ") + normalized_body = textwrap.dedent(body).strip() + indented_body = textwrap.indent(normalized_body, " ") # Generate rationale from patterns rationale = f"Auto-evolved from {len(failures)} failures. " @@ -247,9 +249,16 @@ class EvolutionOptimizer: timestamp=datetime.now(UTC).isoformat(), rationale=rationale, class_name=class_name, - body=indented_body.strip(), + body=indented_body.rstrip(), ) + try: + parsed = ast.parse(content, filename=str(file_path)) + compile(parsed, filename=str(file_path), mode="exec") + except SyntaxError as exc: + logger.warning("Generated strategy failed syntax validation: %s", exc) + return None + file_path.write_text(content) logger.info("Generated strategy file: %s", file_path) return file_path diff --git a/tests/test_evolution.py b/tests/test_evolution.py index 3b10ef1..d5ad349 100644 --- a/tests/test_evolution.py +++ b/tests/test_evolution.py @@ -245,6 +245,52 @@ async def test_generate_strategy_creates_file(optimizer: EvolutionOptimizer, tmp assert "def evaluate" in strategy_path.read_text() +@pytest.mark.asyncio +async def test_generate_strategy_saves_valid_python_code( + optimizer: EvolutionOptimizer, tmp_path: Path, +) -> None: + """Test that syntactically valid generated code is saved.""" + failures = [{"decision_id": "1", "timestamp": "2024-01-15T09:30:00+00:00"}] + + mock_response = Mock() + mock_response.text = ( + 'price = market_data.get("current_price", 0)\n' + 'if price > 0:\n' + ' return {"action": "BUY", "confidence": 80, "rationale": "Positive price"}\n' + 'return {"action": "HOLD", "confidence": 50, "rationale": "No signal"}\n' + ) + + with patch.object(optimizer._client.aio.models, "generate_content", new=AsyncMock(return_value=mock_response)): + with patch("src.evolution.optimizer.STRATEGIES_DIR", tmp_path): + strategy_path = await optimizer.generate_strategy(failures) + + assert strategy_path is not None + assert strategy_path.exists() + + +@pytest.mark.asyncio +async def test_generate_strategy_blocks_invalid_python_code( + optimizer: EvolutionOptimizer, tmp_path: Path, caplog: pytest.LogCaptureFixture, +) -> None: + """Test that syntactically invalid generated code is not saved.""" + failures = [{"decision_id": "1", "timestamp": "2024-01-15T09:30:00+00:00"}] + + mock_response = Mock() + mock_response.text = ( + 'if market_data.get("current_price", 0) > 0\n' + ' return {"action": "BUY", "confidence": 80, "rationale": "broken"}\n' + ) + + with patch.object(optimizer._client.aio.models, "generate_content", new=AsyncMock(return_value=mock_response)): + with patch("src.evolution.optimizer.STRATEGIES_DIR", tmp_path): + with caplog.at_level("WARNING"): + strategy_path = await optimizer.generate_strategy(failures) + + assert strategy_path is None + assert list(tmp_path.glob("*.py")) == [] + assert "failed syntax validation" in caplog.text + + @pytest.mark.asyncio async def test_generate_strategy_handles_api_error(optimizer: EvolutionOptimizer) -> None: """Test that generate_strategy handles Gemini API errors gracefully."""