Files
my-pal-mcp-server/tests/test_debug_continuation.py

339 lines
14 KiB
Python

"""
Test debug tool continuation ID functionality and conversation history formatting.
"""
import json
from unittest.mock import patch
import pytest
from tools.debug import DebugIssueTool
from utils.conversation_memory import (
ConversationTurn,
ThreadContext,
build_conversation_history,
get_conversation_file_list,
)
class TestDebugContinuation:
"""Test debug tool continuation ID and conversation history integration."""
@pytest.mark.asyncio
async def test_debug_creates_continuation_id(self):
"""Test that debug tool creates continuation ID on first step."""
tool = DebugIssueTool()
with patch("utils.conversation_memory.create_thread", return_value="debug-test-uuid-123"):
with patch("utils.conversation_memory.add_turn"):
result = await tool.execute(
{
"step": "Investigating null pointer exception",
"step_number": 1,
"total_steps": 3,
"next_step_required": True,
"findings": "Initial investigation shows null reference in UserService",
"files_checked": ["/api/UserService.java"],
}
)
assert len(result) == 1
response = json.loads(result[0].text)
assert response["status"] == "pause_for_investigation"
assert response["continuation_id"] == "debug-test-uuid-123"
assert response["investigation_required"] is True
assert "required_actions" in response
def test_debug_conversation_formatting(self):
"""Test that debug tool's structured output is properly formatted in conversation history."""
# Create a mock conversation with debug tool output
debug_output = {
"status": "investigation_in_progress",
"step_number": 2,
"total_steps": 3,
"next_step_required": True,
"investigation_status": {
"files_checked": 3,
"relevant_files": 2,
"relevant_methods": 1,
"hypotheses_formed": 1,
"images_collected": 0,
"current_confidence": "medium",
},
"output": {"instructions": "Continue systematic investigation.", "format": "systematic_investigation"},
"continuation_id": "debug-test-uuid-123",
"next_steps": "Continue investigation with step 3.",
}
context = ThreadContext(
thread_id="debug-test-uuid-123",
created_at="2025-01-01T00:00:00Z",
last_updated_at="2025-01-01T00:05:00Z",
tool_name="debug",
turns=[
ConversationTurn(
role="user",
content="Step 1: Investigating null pointer exception",
timestamp="2025-01-01T00:01:00Z",
tool_name="debug",
files=["/api/UserService.java"],
),
ConversationTurn(
role="assistant",
content=json.dumps(debug_output, indent=2),
timestamp="2025-01-01T00:02:00Z",
tool_name="debug",
files=["/api/UserService.java", "/api/UserController.java"],
),
],
initial_context={
"step": "Investigating null pointer exception",
"step_number": 1,
"total_steps": 3,
"next_step_required": True,
"findings": "Initial investigation",
},
)
# Mock file reading to avoid actual file I/O
def mock_read_file(file_path):
if file_path == "/api/UserService.java":
return "// UserService.java\npublic class UserService {\n // code...\n}", 10
elif file_path == "/api/UserController.java":
return "// UserController.java\npublic class UserController {\n // code...\n}", 10
return "", 0
# Build conversation history
from utils.model_context import ModelContext
model_context = ModelContext("flash")
history, tokens = build_conversation_history(context, model_context, read_files_func=mock_read_file)
# Verify the history contains debug-specific content
assert "=== CONVERSATION HISTORY (CONTINUATION) ===" in history
assert "Thread: debug-test-uuid-123" in history
assert "Tool: debug" in history
# Check that files are included
assert "UserService.java" in history
assert "UserController.java" in history
# Check that debug output is included
assert "investigation_in_progress" in history
assert '"step_number": 2' in history
assert '"files_checked": 3' in history
assert '"current_confidence": "medium"' in history
def test_debug_continuation_preserves_investigation_state(self):
"""Test that continuation preserves investigation state across tools."""
# Create a debug investigation context
context = ThreadContext(
thread_id="debug-test-uuid-123",
created_at="2025-01-01T00:00:00Z",
last_updated_at="2025-01-01T00:10:00Z",
tool_name="debug",
turns=[
ConversationTurn(
role="user",
content="Step 1: Initial investigation",
timestamp="2025-01-01T00:01:00Z",
tool_name="debug",
files=["/api/SessionManager.java"],
),
ConversationTurn(
role="assistant",
content=json.dumps(
{
"status": "investigation_in_progress",
"step_number": 1,
"total_steps": 4,
"next_step_required": True,
"investigation_status": {"files_checked": 1, "relevant_files": 1},
"continuation_id": "debug-test-uuid-123",
}
),
timestamp="2025-01-01T00:02:00Z",
tool_name="debug",
),
ConversationTurn(
role="user",
content="Step 2: Found dictionary modification issue",
timestamp="2025-01-01T00:03:00Z",
tool_name="debug",
files=["/api/SessionManager.java", "/api/utils.py"],
),
ConversationTurn(
role="assistant",
content=json.dumps(
{
"status": "investigation_in_progress",
"step_number": 2,
"total_steps": 4,
"next_step_required": True,
"investigation_status": {
"files_checked": 2,
"relevant_files": 1,
"relevant_methods": 1,
"hypotheses_formed": 1,
"current_confidence": "high",
},
"continuation_id": "debug-test-uuid-123",
}
),
timestamp="2025-01-01T00:04:00Z",
tool_name="debug",
),
],
initial_context={},
)
# Get file list to verify prioritization
file_list = get_conversation_file_list(context)
assert file_list == ["/api/SessionManager.java", "/api/utils.py"]
# Mock file reading
def mock_read_file(file_path):
return f"// {file_path}\n// Mock content", 5
# Build history
from utils.model_context import ModelContext
model_context = ModelContext("flash")
history, tokens = build_conversation_history(context, model_context, read_files_func=mock_read_file)
# Verify investigation progression is preserved
assert "Step 1: Initial investigation" in history
assert "Step 2: Found dictionary modification issue" in history
assert '"step_number": 1' in history
assert '"step_number": 2' in history
assert '"current_confidence": "high"' in history
@pytest.mark.asyncio
async def test_debug_to_analyze_continuation(self):
"""Test continuation from debug tool to analyze tool."""
# Simulate debug tool creating initial investigation
debug_context = ThreadContext(
thread_id="debug-analyze-uuid-123",
created_at="2025-01-01T00:00:00Z",
last_updated_at="2025-01-01T00:10:00Z",
tool_name="debug",
turns=[
ConversationTurn(
role="user",
content="Final investigation step",
timestamp="2025-01-01T00:01:00Z",
tool_name="debug",
files=["/api/SessionManager.java"],
),
ConversationTurn(
role="assistant",
content=json.dumps(
{
"status": "calling_expert_analysis",
"investigation_complete": True,
"expert_analysis": {
"status": "analysis_complete",
"summary": "Dictionary modification during iteration bug",
"hypotheses": [
{
"name": "CONCURRENT_MODIFICATION",
"confidence": "High",
"root_cause": "Modifying dict while iterating",
"minimal_fix": "Create list of keys first",
}
],
},
"complete_investigation": {
"initial_issue": "Session validation failures",
"steps_taken": 3,
"files_examined": ["/api/SessionManager.java"],
"relevant_methods": ["SessionManager.cleanup_expired_sessions"],
},
}
),
timestamp="2025-01-01T00:02:00Z",
tool_name="debug",
),
],
initial_context={},
)
# Mock getting the thread
with patch("utils.conversation_memory.get_thread", return_value=debug_context):
# Mock file reading
def mock_read_file(file_path):
return "// SessionManager.java\n// cleanup_expired_sessions method", 10
# Build history for analyze tool
from utils.model_context import ModelContext
model_context = ModelContext("flash")
history, tokens = build_conversation_history(debug_context, model_context, read_files_func=mock_read_file)
# Verify analyze tool can see debug investigation
assert "calling_expert_analysis" in history
assert "CONCURRENT_MODIFICATION" in history
assert "Dictionary modification during iteration bug" in history
assert "SessionManager.cleanup_expired_sessions" in history
# Verify the continuation context is clear
assert "Thread: debug-analyze-uuid-123" in history
assert "Tool: debug" in history # Shows original tool
def test_debug_planner_style_formatting(self):
"""Test that debug tool uses similar formatting to planner for structured responses."""
# Create debug investigation with multiple steps
context = ThreadContext(
thread_id="debug-format-uuid-123",
created_at="2025-01-01T00:00:00Z",
last_updated_at="2025-01-01T00:15:00Z",
tool_name="debug",
turns=[
ConversationTurn(
role="user",
content="Step 1: Initial error analysis",
timestamp="2025-01-01T00:01:00Z",
tool_name="debug",
),
ConversationTurn(
role="assistant",
content=json.dumps(
{
"status": "investigation_in_progress",
"step_number": 1,
"total_steps": 3,
"next_step_required": True,
"output": {
"instructions": "Continue systematic investigation.",
"format": "systematic_investigation",
},
"continuation_id": "debug-format-uuid-123",
},
indent=2,
),
timestamp="2025-01-01T00:02:00Z",
tool_name="debug",
),
],
initial_context={},
)
# Build history
from utils.model_context import ModelContext
model_context = ModelContext("flash")
history, _ = build_conversation_history(context, model_context, read_files_func=lambda x: ("", 0))
# Verify structured format is preserved
assert '"status": "investigation_in_progress"' in history
assert '"format": "systematic_investigation"' in history
assert "--- Turn 1 (Claude using debug) ---" in history
assert "--- Turn 2 (Gemini using debug" in history
# The JSON structure should be preserved for tools to parse
# This allows other tools to understand the investigation state
turn_2_start = history.find("--- Turn 2 (Gemini using debug")
turn_2_content = history[turn_2_start:]
assert "{\n" in turn_2_content # JSON formatting preserved
assert '"continuation_id"' in turn_2_content