Lots of tests with live simulation to validate conversation continuation / preservation work across requests

This commit is contained in:
Fahad
2025-06-11 17:16:05 +04:00
parent c90ac7561e
commit 780000f9c9
15 changed files with 272 additions and 2296 deletions

View File

@@ -2,11 +2,11 @@
Enhanced tests for precommit tool using mock storage to test real logic
"""
import json
import tempfile
import os
from unittest.mock import Mock, patch, MagicMock
from typing import Dict, Any, Optional
import tempfile
from pathlib import Path
from typing import Optional
from unittest.mock import patch
import pytest
@@ -15,60 +15,70 @@ from tools.precommit import Precommit, PrecommitRequest
class MockRedisClient:
"""Mock Redis client that uses in-memory dictionary storage"""
def __init__(self):
self.data: Dict[str, str] = {}
self.ttl_data: Dict[str, int] = {}
self.data: dict[str, str] = {}
self.ttl_data: dict[str, int] = {}
def get(self, key: str) -> Optional[str]:
return self.data.get(key)
def set(self, key: str, value: str, ex: Optional[int] = None) -> bool:
self.data[key] = value
if ex:
self.ttl_data[key] = ex
return True
def delete(self, key: str) -> int:
if key in self.data:
del self.data[key]
self.ttl_data.pop(key, None)
return 1
return 0
def exists(self, key: str) -> int:
return 1 if key in self.data else 0
def setex(self, key: str, time: int, value: str) -> bool:
"""Set key to hold string value and set key to timeout after given seconds"""
self.data[key] = value
self.ttl_data[key] = time
return True
class TestPrecommitToolWithMockStore:
"""Test precommit tool with mock storage to validate actual logic"""
@pytest.fixture
def mock_redis(self):
"""Create mock Redis client"""
return MockRedisClient()
@pytest.fixture
def tool(self, mock_redis):
def tool(self, mock_redis, temp_repo):
"""Create tool instance with mocked Redis"""
temp_dir, _ = temp_repo
tool = Precommit()
# Mock the Redis client getter to return our mock
with patch('utils.conversation_memory.get_redis_client', return_value=mock_redis):
# Mock the Redis client getter and PROJECT_ROOT to allow access to temp files
with (
patch("utils.conversation_memory.get_redis_client", return_value=mock_redis),
patch("utils.file_utils.PROJECT_ROOT", Path(temp_dir).resolve()),
):
yield tool
@pytest.fixture
def temp_repo(self):
"""Create a temporary git repository with test files"""
import subprocess
temp_dir = tempfile.mkdtemp()
# Initialize git repo
subprocess.run(['git', 'init'], cwd=temp_dir, capture_output=True)
subprocess.run(['git', 'config', 'user.name', 'Test'], cwd=temp_dir, capture_output=True)
subprocess.run(['git', 'config', 'user.email', 'test@example.com'], cwd=temp_dir, capture_output=True)
subprocess.run(["git", "init"], cwd=temp_dir, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=temp_dir, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=temp_dir, capture_output=True)
# Create test config file
config_content = '''"""Test configuration file"""
@@ -80,182 +90,173 @@ __author__ = "Test"
MAX_CONTENT_TOKENS = 800_000 # 800K tokens for content
TEMPERATURE_ANALYTICAL = 0.2 # For code review, debugging
'''
config_path = os.path.join(temp_dir, 'config.py')
with open(config_path, 'w') as f:
config_path = os.path.join(temp_dir, "config.py")
with open(config_path, "w") as f:
f.write(config_content)
# Add and commit initial version
subprocess.run(['git', 'add', '.'], cwd=temp_dir, capture_output=True)
subprocess.run(['git', 'commit', '-m', 'Initial commit'], cwd=temp_dir, capture_output=True)
subprocess.run(["git", "add", "."], cwd=temp_dir, capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=temp_dir, capture_output=True)
# Modify config to create a diff
modified_content = config_content + '\nNEW_SETTING = "test" # Added setting\n'
with open(config_path, 'w') as f:
with open(config_path, "w") as f:
f.write(modified_content)
yield temp_dir, config_path
# Cleanup
import shutil
shutil.rmtree(temp_dir)
@pytest.mark.asyncio
async def test_no_duplicate_file_content_in_prompt(self, tool, temp_repo, mock_redis):
"""Test that file content doesn't appear twice in the generated prompt"""
"""Test that file content appears in expected locations"""
temp_dir, config_path = temp_repo
# Create request with files parameter
request = PrecommitRequest(
path=temp_dir,
files=[config_path],
original_request="Test configuration changes"
)
# Create request with files parameter
request = PrecommitRequest(path=temp_dir, files=[config_path], original_request="Test configuration changes")
# Generate the prompt
prompt = await tool.prepare_prompt(request)
# Test that MAX_CONTENT_TOKENS only appears once in the entire prompt
max_content_count = prompt.count('MAX_CONTENT_TOKENS = 800_000')
assert max_content_count == 1, f"MAX_CONTENT_TOKENS appears {max_content_count} times (should be 1)"
# Test that the config file content only appears once
config_content_count = prompt.count('# Configuration')
assert config_content_count == 1, f"Config file content appears {config_content_count} times (should be 1)"
# Verify expected sections are present
assert "## Original Request" in prompt
assert "Test configuration changes" in prompt
assert "## Additional Context Files" in prompt
assert "## Git Diffs" in prompt
# Verify the file appears in the git diff
assert "config.py" in prompt
assert "NEW_SETTING" in prompt
# Note: Files can legitimately appear in both git diff AND additional context:
# - Git diff shows only changed lines + limited context
# - Additional context provides complete file content for full understanding
# This is intentional and provides comprehensive context to the AI
@pytest.mark.asyncio
async def test_conversation_memory_integration(self, tool, temp_repo, mock_redis):
"""Test that conversation memory works with mock storage"""
temp_dir, config_path = temp_repo
# Mock conversation memory functions to use our mock redis
with patch('utils.conversation_memory.get_redis_client', return_value=mock_redis):
with patch("utils.conversation_memory.get_redis_client", return_value=mock_redis):
# First request - should embed file content
request1 = PrecommitRequest(
path=temp_dir,
files=[config_path],
original_request="First review"
)
PrecommitRequest(path=temp_dir, files=[config_path], original_request="First review")
# Simulate conversation thread creation
from utils.conversation_memory import create_thread, add_turn
from utils.conversation_memory import add_turn, create_thread
thread_id = create_thread("precommit", {"files": [config_path]})
# Test that file embedding works
files_to_embed = tool.filter_new_files([config_path], None)
assert config_path in files_to_embed, "New conversation should embed all files"
# Add a turn to the conversation
add_turn(thread_id, "assistant", "First response", files=[config_path], tool_name="precommit")
# Second request with continuation - should skip already embedded files
request2 = PrecommitRequest(
path=temp_dir,
files=[config_path],
continuation_id=thread_id,
original_request="Follow-up review"
PrecommitRequest(
path=temp_dir, files=[config_path], continuation_id=thread_id, original_request="Follow-up review"
)
files_to_embed_2 = tool.filter_new_files([config_path], thread_id)
assert len(files_to_embed_2) == 0, "Continuation should skip already embedded files"
@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_prompt_structure_integrity(self, tool, temp_repo, mock_redis):
"""Test that the prompt structure is well-formed and doesn't have content duplication"""
temp_dir, config_path = temp_repo
request = PrecommitRequest(
path=temp_dir,
files=[config_path],
original_request="Validate prompt structure",
review_type="full",
severity_filter="high"
severity_filter="high",
)
prompt = await tool.prepare_prompt(request)
# Split prompt into sections
sections = {
"original_request": "## Original Request",
"review_parameters": "## Review Parameters",
"review_parameters": "## Review Parameters",
"repo_summary": "## Repository Changes Summary",
"context_files_summary": "## Context Files Summary",
"git_diffs": "## Git Diffs",
"additional_context": "## Additional Context Files",
"review_instructions": "## Review Instructions"
"review_instructions": "## Review Instructions",
}
section_indices = {}
for name, header in sections.items():
index = prompt.find(header)
if index != -1:
section_indices[name] = index
# Verify sections appear in logical order
assert section_indices["original_request"] < section_indices["review_parameters"]
assert section_indices["review_parameters"] < section_indices["repo_summary"]
assert section_indices["review_parameters"] < section_indices["repo_summary"]
assert section_indices["git_diffs"] < section_indices["additional_context"]
assert section_indices["additional_context"] < section_indices["review_instructions"]
# Test that file content only appears in Additional Context section
file_content_start = section_indices["additional_context"]
file_content_end = section_indices["review_instructions"]
file_section = prompt[file_content_start:file_content_end]
before_file_section = prompt[:file_content_start]
prompt[:file_content_start]
after_file_section = prompt[file_content_end:]
# MAX_CONTENT_TOKENS should only appear in the file section
assert 'MAX_CONTENT_TOKENS' in file_section
assert 'MAX_CONTENT_TOKENS' not in before_file_section
assert 'MAX_CONTENT_TOKENS' not in after_file_section
# File content should appear in the file section
assert "MAX_CONTENT_TOKENS = 800_000" in file_section
# Check that configuration content appears in the file section
assert "# Configuration" in file_section
# The complete file content should not appear in the review instructions
assert '__version__ = "1.0.0"' in file_section
assert '__version__ = "1.0.0"' not in after_file_section
@pytest.mark.asyncio
async def test_file_content_formatting(self, tool, temp_repo, mock_redis):
"""Test that file content is properly formatted without duplication"""
temp_dir, config_path = temp_repo
# Test the centralized file preparation method directly
file_content = tool._prepare_file_content_for_prompt(
[config_path],
None, # No continuation
"Test files",
max_tokens=100000,
reserve_tokens=1000
[config_path], None, "Test files", max_tokens=100000, reserve_tokens=1000 # No continuation
)
# Should contain file markers
assert "--- BEGIN FILE:" in file_content
assert "--- END FILE:" in file_content
assert "config.py" in file_content
# Should contain actual file content
assert "MAX_CONTENT_TOKENS = 800_000" in file_content
assert "__version__ = \"1.0.0\"" in file_content
assert '__version__ = "1.0.0"' in file_content
# Content should appear only once
assert file_content.count("MAX_CONTENT_TOKENS = 800_000") == 1
assert file_content.count("__version__ = \"1.0.0\"") == 1
assert file_content.count('__version__ = "1.0.0"') == 1
def test_mock_redis_basic_operations():
"""Test that our mock Redis implementation works correctly"""
mock_redis = MockRedisClient()
# Test basic operations
assert mock_redis.get("nonexistent") is None
assert mock_redis.exists("nonexistent") == 0
mock_redis.set("test_key", "test_value")
assert mock_redis.get("test_key") == "test_value"
assert mock_redis.exists("test_key") == 1
assert mock_redis.delete("test_key") == 1
assert mock_redis.get("test_key") is None
assert mock_redis.delete("test_key") == 0 # Already deleted
assert mock_redis.delete("test_key") == 0 # Already deleted